引言
一直以來都是在談如何開發(fā), 如開發(fā)的小技巧小經(jīng)驗
今天為什么突然說起編程規(guī)范來了呢?
因為在我看來, 編程規(guī)范是反映一個開發(fā)者或是一個開發(fā)團(tuán)隊素質(zhì)和成熟度的最重要標(biāo)志!
注意這里的措辭很嚴(yán)厲哈, 我用了"最重要"
當(dāng)我們看到各種文不表意的函數(shù)命名、剪不斷理還亂的文件和代碼結(jié)構(gòu)绑青、以及不符合Apple官方基本規(guī)范的風(fēng)格時
我想說: 程序員的加班 -- 你懂得(開發(fā)者制造的問題往往比他們解決的問題要多)
如果讀者覺得有比編程規(guī)范更重要的事情, 可以來辯, 歡迎拍磚~
本文的代碼規(guī)范基于The official raywenderlich.com Objective-C style guide, 但不局限于此, 結(jié)合實際開發(fā)遇到的問題, 做了部分修訂和補(bǔ)充
背景
Apple官方的代碼規(guī)范, 供補(bǔ)充參考:
- The Objective-C Programming Language
- Cocoa Fundamentals Guide
- Coding Guidelines for Cocoa
- iOS App Programming Guide
目錄
- 語言
- 代碼組織
- 空格
- 注釋
- 命名
- 方法
- 變量
- 屬性
- 點語法
- Object Literals
- 常量
- 枚舉
- Case表達(dá)式
- 私有成員
- 布爾
- 條件語句
- 初始化方法
- 類構(gòu)造方法
- CGRect函數(shù)
- Golden Path
- 錯誤處理
- 單例
- 換行
- 頭文件
- 版權(quán)聲明
- Xcode Project
語言
使用美式英語
Preferred:
UIColor *myColor = [UIColor whiteColor];
Not Preferred:
UIColor *myColour = [UIColor whiteColor];
代碼組織
使用#pragma mark -
對function/protocol/delegate進(jìn)行分組
#pragma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
#pragma mark - IBActions
- (IBAction)submitData:(id)sender {}
#pragma mark - Public
- (void)publicMethod {}
#pragma mark - Private
- (void)privateMethod {}
#pragma mark - Protocol
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
空格
條件表達(dá)式的括號 (如: if
/else
/switch
/while
), 左括號和表達(dá)式在同一行, 右括號另起一行
Preferred:
if (user.isHappy) {
// Do something
} else {
// Do something else
}
Not Preferred:
if (user.isHappy)
{
// Do something
}
else {
// Do something else
}
避免使用冒號對齊方式, 除非代碼過長需要換行, 但是不要讓block也同樣保持冒號對齊
Preferred:
// blocks are easily readable
[UIView animateWithDuration:1.0f animations:^{
// something
} completion:^(BOOL finished) {
// something
}];
Not Preferred:
// colon-aligning makes the block indentation hard to read
[UIView animateWithDuration:1.0
animations:^{
// something
}
completion:^(BOOL finished) {
// something
}];
- 簽代理
Preferred:
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer>
@property (nonatomic, assign) id<ChooseProvinceViewControllerdDelegate> delegate;
Not Preferred:
@interface UIViewController : UIResponder<NSCoding,UIAppearanceContainer>
@property (nonatomic, assign) id <ChooseProvinceViewControllerdDelegate> delegate;
注釋
確實需要, 才加注釋
注釋是用來解釋why, 而不是what
Preferred:
// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
Not Preferred:
/**
* show head image
*/
@property (nonatomic, strong) DDImageView *headView;
// im conversation done edit
- (void)imConversationDoneEdit;
- 添加的注釋要保持更新或刪除
命名
- 使用長且描述性強(qiáng)的命名方式
Preferred:
UIButton *settingsButton;
Not Preferred:
UIButton *setBut;
- 在類名和常量名前加上3個字母的前綴(Core Data entity names去掉前綴)
需要統(tǒng)一整個工程的命名前綴, 因為OC里沒有namespace, 工程越龐大后果越嚴(yán)重
- 常量的命名采用"駝峰"的命名方式
Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3f;
Not Preferred:
static NSTimeInterval const fadetime = 1.7f;
成員的命名采用首字母小寫的"駝峰"命名方式, 并且使用auto-synthesis方式聲明成員, 而不是@synthesize方式
Preferred:
@property (nonatomic, copy) NSString *descriptiveVariableName;
Not Preferred:
id varnm;
命名時注意存儲數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)發(fā)生改變的時候岛宦,會不會引起命名的改變。
Preferred:
@property (nonatomic, strong) NSArray *actionSheetItems;
@property(nullable, nonatomic, copy) NSArray<UIBarButtonItem *> *items;
Not Preferred:
@property (nonatomic, strong) NSArray *actionSheetItemArray;
圖標(biāo)資源命名
模塊名+功能名+[狀態(tài)],( [ ] : 表明可選)
圖標(biāo)在Xcode里面的名稱需要與圖標(biāo)的物理命名保持一致
Preferred:
addressbook_isvnetwork // 指代沒有狀態(tài)的圖標(biāo)名
addressbook_call_normal
addressbook_call_highlight // 指代有多種狀態(tài)的圖標(biāo)名
conference_member_delete_normal
conference_member_delete_highlight // 指代功能名較長且有多種狀態(tài)的圖標(biāo)名
下劃線
使用self.
方式來訪問對象的成員(參考點語法), 從視覺上就可以區(qū)分出哪些是本對象成員
注意:
在初始化方法(
init
,initWithCoder:
等),dealloc
以及setters/getters方法中中必須使用保留的_variableName局部變量不要用下劃線開頭! 因為下劃線開頭的命名方式是Apple保留的命名方式
方法
方法類型(-/+)的后面有個空格
每一個參數(shù)都要有描述
注意:
- 不要在多個參數(shù)的描述中添加"and", 例如下面的
initWithWidth:height:
方法
Preferred:
- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
Not Preferred:
-(void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height; // Never do this.
變量
變量命名要可讀性強(qiáng), 避免用單個單詞的命名方式, 除了在for()
循環(huán)里
指針的星號和變量名連在一起, 例如: NSString *text
, 而不是NSString* text
, NSString * text
避免直接使用_viriableName方式訪問對象的成員, 除了在初始化方法(init
, initWithCoder:
等), dealloc
方法以及setters/getters方法中點語法
了解更多關(guān)于在初始化和dealloc中使用accessor方法, 請參考這里.
Preferred:
@interface RWTTutorial : NSObject
@property (nonatomic, copy) NSString *tutorialName;
@property (nonatomic, copy) NSArray<UIBarButtonItem *> *items;
@end
Not Preferred:
@interface RWTTutorial : NSObject {
NSString *tutorialName;
}
如果變量是一個度量的話(如按時間長度或者字節(jié)數(shù))子寓,那么最好把名字帶上它的單位
Preferred:
NSString *durationOfSeconds;
Not Preferred:
NSString *duration;
屬性
屬性要按照先atomicity后storage的順序, 這是為了和Apple的Interface Builder生成的代碼保持一致.不用對齊,根據(jù)功能用空格實現(xiàn)分組
Preferred:
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nullable, readonly, nonatomic, copy) NSString *tutorialName;
Not Preferred:
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (nonatomic) NSString *tutorialName;
注意:
- 控件的storage屬性通常設(shè)置為weak, 而非strong
如果成員引用的對象是可變對象, 那么需要使用copy
而非strong
Why?
因為即使成員的類型是NSString
, 但實際傳入的如果是NSMutableString
的對象
該對象在你不知情的情況下, 仍然會被修改
Preferred:
@property (nonatomic, copy) NSString *tutorialName;
Not Preferred:
@property (nonatomic, strong) NSString *tutorialName;
點語法
點語法實際是對accessor方法的封裝
了解更多點語法, 請參考這里
訪問和操作對象成員, 推薦使用點語法; Bracket notation is preferred in all other instances.
Preferred:
NSInteger arrayCount = [self.array count];
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
Not Preferred:
NSInteger arrayCount = self.array.count;
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
Object Literals
使用Object Literals(@
)方式來快速創(chuàng)建NSString
, NSDictionary
, NSArray
, NSNumber
實例
注意:
- 如果傳給
NSArray
,NSDictionary
的值是nil會導(dǎo)致crash
Preferred:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;
Not Preferred:
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingStreetNumber = [NSNumber numberWithInteger:10018];
- 容器類型最好標(biāo)明存儲數(shù)據(jù)的類型
Preferred:
@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForVisibleRows;
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView ;
- 在數(shù)組和字典中, 使用索引和關(guān)鍵字來獲取數(shù)據(jù)
Preferred:
NSString *name = names[1];
NSString *product = [productManagers valueForKey:@"kate"]
Not Preferred:
NSString *name = [names objectAtIndex:1];
NSString *product = [productManagers objectForKey:@"@kate"]
常量
使用static
而非#define
來聲明常量; unless explicitly being used as a macro
用const修飾時,const右邊的總是不能被修改.聲明時不用對齊,根據(jù)功能用空格實現(xiàn)分組
Preferred:
static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
static CGFloat const RWTImageThumbnailHeight = 50.0f;
Not Preferred:
#define CompanyName @"RayWenderlich.com"
#define thumbnailHeight 2.0f
- 宏定義
使用大寫字母食零,用_分割單詞,宏定義中如果包含表達(dá)式或變量蝗肪,表達(dá)式和變量必須用小括號括起來
不到萬不得已不推薦使用宏定義像數(shù)字常量袜爪,通知的參數(shù)一般不推薦使用宏定義,推薦使用static const 的形式
Preferred:
#define SCREEN_RECT ([UIScreen mainScreen].bounds)
#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)
- 浮點數(shù)
使用浮點數(shù)在數(shù)值的后面加上f,用來區(qū)別double類型
Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3f;
Not Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3;
枚舉
使用NS_ENUM()
聲明枚舉, 因為它具有更強(qiáng)的類型檢查機(jī)制
For Example:
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
當(dāng)然你還是可以顯式地設(shè)置枚舉的值
typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
RWTPinSizeMin = 1,
RWTPinSizeMax = 5,
RWTPinCountMin = 100,
RWTPinCountMax = 500,
};
不要使用老式的枚舉定義方式, 除非編寫CoreFoundation C的代碼
Not Preferred:
enum GlobalConstants {
kMaxPinSize = 5,
kMaxPinCount = 500,
};
Case表達(dá)式
Case表達(dá)式通常不需要加括號
Case內(nèi)容有if判斷句, for循環(huán)和局部變量時, 需要加上括號 (左括號的右邊, break放在括號外). break與下一個case之間有空行.
switch (condition) {
case 1: {
for () {
// ...
}
}
break;
case 2: {
if {
//
}
}
break;
case 3:
// ...
break;
default:
// ...
break;
}
多個Case表達(dá)式執(zhí)行相同的邏輯, 使用fall-through
方式(即刪除表達(dá)式里的break)
此時, 需要加上注釋說明注釋
switch (condition) {
case 1:
// ** fall-through! **
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
私有成員
私有屬性要在類的實現(xiàn)中聲明: 放在class extension(即匿名category)中
For Example:
@interface RWTDetailViewController ()
@property (nonatomic, strong) GADBannerView *googleAdView;
@property (nonatomic, strong) ADBannerView *iAdView;
@property (nonatomic, strong) UIWebView *adXWebView;
@end
布爾
Objective-C使用YES
和NO
, 而true
和false
只用在CoreFoundation, C或C++代碼里
Since nil
resolves to NO
it is unnecessary to compare it in conditions. Never compare something directly to YES
, because YES
is defined to 1 and a BOOL
can be up to 8 bits.
This allows for more consistency across files and greater visual clarity.
Preferred:
if (someObject) {}
if (![anotherObject boolValue]) {}
Not Preferred:
if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this
if (isAwesome == true) {} // Never do this
如果BOOL
類型的屬性是一個形容詞, 那么可以去掉"is"前綴, 但get accessor中仍然需要保留前綴
@property (assign, getter=editable) BOOL editable;
條件語句
條件語句要加上括號, 即使是一行語句, 否則會存在隱患: even more dangerous defect
Preferred:
if (!error) {
return success;
}
Not Preferred:
if (!error)
return success;
or
if (!error) return success;
三元運算符
如果可以使代碼變得簡潔和高效, 那么可以考慮使用三元運算符?:
, 否則還是用if
表達(dá)式
通常, 給變量賦值時, 可以考慮使用三元運算符?:
Preferred:
NSInteger value = 5;
result = (value != 0) ? x : y;
BOOL isHorizontal = YES;
result = isHorizontal ? x : y;
Not Preferred:
result = a > b ? x = c > d ? c : d : y;
初始化方法
初始化方法要和Apple模板保持一致
- (instancetype)init {
self = [super init];
if (self) {
// Do something
}
return self;
}
返回值類型是'instancetype'而不是'id'
關(guān)于instancetype, 請參考類構(gòu)造方法
類構(gòu)造方法
類構(gòu)造方法返回值類型是'instancetype'而不是'id', 這樣可以幫忙編譯器推導(dǎo)出返回值得類型
@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end
了解更多關(guān)于instancetype: NSHipster.com.
CGRect函數(shù)
要獲取CGRect
的x
, y
, width
, height
, 不要直接訪問結(jié)構(gòu)體的成員, 而應(yīng)該使用CGGeometry functions
Apple官方對于CGGeometry
的解釋:
All functions described in this reference that take CGRect data structures as inputs implicitly standardize those rectangles before calculating their results. For this reason, your applications should avoid directly reading and writing the data stored in the CGRect data structure. Instead, use the functions described here to manipulate rectangles and to retrieve their characteristics.
Preferred:
CGRect frame = self.view.frame;
CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0f, 0.0f, width, height);
Not Preferred:
CGRect frame = self.view.frame;
CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };
Golden Path
如果把需要執(zhí)行的邏輯比作"golden"或"happy" path
那么在判斷條件不滿足時, 直接return退出方法, 而不是判斷條件滿足時, 執(zhí)行該Golden Path
Preferred:
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
// Do something important
}
Not Preferred:
- (void)someMethod {
if ([someOther boolValue]) {
// Do something important
}
}
錯誤處理
通過方法的返回值而不是返回的error引用, 來做錯誤處理的判斷條件
Preferred:
NSError *error;
if (![self trySomethingWithError:&error]) {
// Handle Error
}
Not Preferred:
NSError *error;
[self trySomethingWithError:&error];
if (error) {
// Handle Error
}
單例
單例對象的實例化必須要確保線程安全
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
否則可能會出現(xiàn)這樣的問題possible and sometimes prolific crashes.
換行
頭文件中的方法, 使用冒號對齊的方式
Preferred:
- (void)setViewWithHeadImageUrl:(NSString *)headImageUrl
name:(NSString *)name
phoneNumber:(NSString *)phoneNumber;
Not Preferred:
- (void)setViewWithHeadImageUrl:(NSString *)headImageUrl name:(NSString *)name phoneNumber:(NSString *)phoneNumber;
實現(xiàn)文件或方法調(diào)用時, 不用冒號對齊
Preferred:
[[MSDBHelper sharedInstance] updateCallRecordsWithNumber:number isPersonalContact:YES oldName:oldContact.displayName toNewName:@""];
Not Preferred:
[[MSDBHelper sharedInstance] updateCallRecordsWithNumber:number
isPersonalContact:YES
oldName:oldContact.displayName
toNewName:@""];
如果語句過長, 需要考慮代碼的分解和優(yōu)化
Preferred:
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[ctpManager connectToHost:kHostNameCloudPhoneNoport onPort:kCTPPort userInfo:userInfo;
[userInfo setObject:[AccountLoginModel sharedInstance].userInfo.mobilephone forKey:CTP_AUTHUSER_USER_NAME, nil];
Not Preferred:
[ctpManager connectToHost:kHostNameCloudPhoneNoport onPort:kCTPPort userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[AccountLoginModel sharedInstance].userInfo.mobilephone, CTP_AUTHUSER_USER_NAME, nil];
頭文件
自定義頭文件放在系統(tǒng)頭文件的前面
按功能分開薛闪,加空格以示區(qū)分
按字母表排序
Preferred:
#import "DailModel"
#import "DialView.h"
#import "DialViewController.h"
#import <QYCTPManager/CTPManager.h>
Not Preferred:
#import <QYCTPManager/CTPManager.h>
#import "DialView.h"
#import "DailModel"
#import "DialViewController.h"
不要引入無關(guān)的頭文件
盡量使用向前聲明取代引入辛馆,這樣不僅可以縮減編譯時間,而且還能降低彼此依賴程度
版權(quán)聲明
Preferred:
// MSDetailRecordsViewController.m
// CloudPhone
//
// Created by chenguang (guochenguang@qiyoukeji.com) on 15-12-6.
// Copyright (c) 2015年 QIYOU Ltd. All rights reserved.
//
Not Preferred:
// MSDetailRecordsViewController.m
// chenguang
//
// Created by chenguang on 15-12-6.
// Copyright (c) 2015年 CloudPhone. All rights reserved.
//
Xcode Project
Xcode Group要和文件系統(tǒng)里的文件夾關(guān)聯(lián)起來
代碼不僅要按照類型分組, 也要按照功能和特性進(jìn)行分組
如果可以的話, 打開Build Settings里的"Treat Warnings as Errors"選項
并且盡可能多的打開additional warnings
如果需要忽略特定的warning, 請參考Clang's pragma feature.
其他Objective-C編程規(guī)范
- Robots & Pencils
- New York Times
- GitHub
- Adium
- Sam Soffes
- CocoaDevCentral
- Luke Redpath
- Marcus Zarra
更多文章, 請支持我的個人博客