由於我正在準備模仿餓了麼這個app,到時可能有些iOS開發者參與進來。這時如果每個人的Objective-C編碼風格都不一樣,這樣不易於保持程式碼一致性和難以Code Review。所以我在網上搜尋到 The official raywenderlich.com Objective-C style guide這篇關於Objective-C編碼風格的文章,覺得可以作為這個專案的Objective-C的編碼標準,所以就翻譯這篇文章。
raywenderlich.com Objective-C編碼規範
這篇編碼風格指南概括了raywenderlich.com的編碼規範,可能有些刪減或修改。
介紹
我們制定Objective-C編碼規範的原因是我們能夠在我們的書,教程和初學者工具包的程式碼保持優雅和一致。即使我們有很多不同的作者來完成不同的書籍。
這裡編碼規範有可能與你看到的其他Objective-C編碼規範不同,因為它主要是為了列印和web的易讀性。
關於作者
這編碼規範的建立是由很多來自raywenderlich.com團隊成員在Nicholas Waynik的帶領下共同完成的。團隊成員有:Soheil Moayedi Azarpour, Ricardo Rendon Cepeda, Tony Dahbura, Colin Eberhardt, Matt Galloway, Greg Heo, Matthijs Hollemans, Christopher LaPollo, Saul Mora, Andy Pereira, Mic Pringle, Pietro Rea, Cesare Rocchi, Marin Todorov, Nicholas Waynik和Ray Wenderlich
我們也非常感謝New York Times 和Robots & Pencils’Objective-C編碼規範的作者。這兩個編碼規範為本指南的建立提供很好的起點。
背景
這裡有些關於編碼風格Apple官方文件,如果有些東西沒有提及,可以在以下文件來查詢更多細節:
- The Objective-C Programming Language
- Cocoa Fundamentals Guide
- Coding Guidelines for Cocoa
- iOS App Programming Guide
語言
應該使用US英語.
應該:
1 |
UIColor *myColor = [UIColor whiteColor]; |
不應該:
1 |
UIColor *myColour = [UIColor whiteColor]; |
程式碼組織
在函式分組和protocol/delegate實現中使用#pragma mark -
來分類方法,要遵循以下一般結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#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 conformance #pragma mark - UITextFieldDelegate #pragma mark - UITableViewDataSource #pragma mark - UITableViewDelegate #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone {} #pragma mark - NSObject - (NSString *)description {} |
空格
- 縮排使用4個空格,確保在Xcode偏好設定來設定。(raywenderlich.com使用2個空格)
- 方法大括號和其他大括號(
if
/else
/switch
/while
等.)總是在同一行語句開啟但在新行中關閉。
應該:
1 2 3 4 5 |
if (user.isHappy) { //Do something } else { //Do something else } |
不應該:
1 2 3 4 5 6 7 |
if (user.isHappy) { //Do something } else { //Do something else } |
- 在方法之間應該有且只有一行,這樣有利於在視覺上更清晰和更易於組織。在方法內的空白應該分離功能,但通常都抽離出來成為一個新方法。
- 優先使用auto-synthesis。但如果有必要,
@synthesize
和@dynamic
應該在實現中每個都宣告新的一行。 - 應該避免以冒號對齊的方式來呼叫方法。因為有時方法簽名可能有3個以上的冒號和冒號對齊會使程式碼更加易讀。請不要這樣做,儘管冒號對齊的方法包含程式碼塊,因為Xcode的對齊方式令它難以辨認。
應該:
1 2 3 4 5 6 |
// blocks are easily readable [UIView animateWithDuration:1.0 animations:^{ // something } completion:^(BOOL finished) { // something }]; |
不應該:
1 2 3 4 5 6 7 8 |
// colon-aligning makes the block indentation hard to read [UIView animateWithDuration:1.0 animations:^{ // something } completion:^(BOOL finished) { // something }]; |
註釋
當需要註釋時,註釋應該用來解釋這段特殊程式碼為什麼要這樣做。任何被使用的註釋都必須保持最新或被刪除。
一般都避免使用塊註釋,因為程式碼儘可能做到自解釋,只有當斷斷續續或幾行程式碼時才需要註釋。例外:這不應用在生成文件的註釋
命名
Apple命名規則儘可能堅持,特別是與這些相關的memory management rules(NARC)。
長的,描述性的方法和變數命名是好的。
應該:
1 |
UIButton *settingsButton; |
不應該:
1 |
UIButton *setBut; |
三個字元字首應該經常用在類和常量命名,但在Core Data的實體名中應被忽略。對於官方的raywenderlich.com書、初學者工具包或教程,字首’RWT’應該被使用。
常量應該使用駝峰式命名規則,所有的單詞首字母大寫和加上與類名有關的字首。
應該:
1 |
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3; |
不應該:
1 |
static NSTimeInterval const fadetime = 1.7; |
屬性也是使用駝峰式,但首單詞的首字母小寫。對屬性使用auto-synthesis,而不是手動編寫@ synthesize語句,除非你有一個好的理由。
應該:
1 |
@property (strong, nonatomic) NSString *descriptiveVariableName; |
不應該:
1 |
id varnm; |
下劃線
當使用屬性時,例項變數應該使用self.
來訪問和改變。這就意味著所有屬性將會視覺效果不同,因為它們前面都有self.
。
但有一個特例:在初始化方法裡,例項變數(例如,_variableName)應該直接被使用來避免getters/setters潛在的副作用。
區域性變數不應該包含下劃線。
方法
在方法簽名中,應該在方法型別(-/+ 符號)之後有一個空格。在方法各個段之間應該也有一個空格(符合Apple的風格)。在引數之前應該包含一個具有描述性的關鍵字來描述引數。
“and”這個詞的用法應該保留。它不應該用於多個引數來說明,就像initWithWidth:height
以下這個例子:
應該:
1 2 3 4 |
- (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; |
不應該:
1 2 3 4 5 |
-(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. |
變數
變數儘量以描述性的方式來命名。單個字元的變數命名應該儘量避免,除了在for()
迴圈。
星號表示變數是指標。例如, NSString *text
既不是 NSString* text
也不是 NSString * text
,除了一些特殊情況下常量。
私有變數 應該儘可能代替例項變數的使用。儘管使用例項變數是一種有效的方式,但更偏向於使用屬性來保持程式碼一致性。
通過使用’back’屬性(_variable,變數名前面有下劃線)直接訪問例項變數應該儘量避免,除了在初始化方法(init
, initWithCoder:
, 等…),dealloc
方法和自定義的setters和getters。想了解關於如何在初始化方法和dealloc直接使用Accessor方法的更多資訊,檢視這裡
應該:
1 2 3 4 5 |
@interface RWTTutorial : NSObject @property (strong, nonatomic) NSString *tutorialName; @end |
不應該:
1 2 3 |
@interface RWTTutorial : NSObject { NSString *tutorialName; } |
屬性特性
所有屬性特性應該顯式地列出來,有助於新手閱讀程式碼。屬性特性的順序應該是storage、atomicity,與在Interface Builder連線UI元素時自動生成程式碼一致。
應該:
1 2 |
@property (weak, nonatomic) IBOutlet UIView *containerView; @property (strong, nonatomic) NSString *tutorialName; |
不應該:
1 2 |
@property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic) NSString *tutorialName; |
NSString應該使用copy
而不是 strong
的屬性特性。
為什麼?即使你宣告一個NSString
的屬性,有人可能傳入一個NSMutableString
的例項,然後在你沒有注意的情況下修改它。
應該:
1 |
@property (copy, nonatomic) NSString *tutorialName; |
不應該:
1 |
@property (strong, nonatomic) NSString *tutorialName; |
點符號語法
點語法是一種很方便封裝訪問方法呼叫的方式。當你使用點語法時,通過使用getter或setter方法,屬性仍然被訪問或修改。想了解更多,閱讀這裡
點語法應該總是被用來訪問和修改屬性,因為它使程式碼更加簡潔。[]符號更偏向於用在其他例子。
應該:
1 2 3 |
NSInteger arrayCount = [self.array count]; view.backgroundColor = [UIColor orangeColor]; [UIApplication sharedApplication].delegate; |
不應該:
1 2 3 |
NSInteger arrayCount = self.array.count; [view setBackgroundColor:[UIColor orangeColor]]; UIApplication.sharedApplication.delegate; |
字面值
NSString
, NSDictionary
, NSArray
, 和 NSNumber
的字面值應該在建立這些類的不可變例項時被使用。請特別注意nil
值不能傳入NSArray
和NSDictionary
字面值,因為這樣會導致crash。
應該:
1 2 3 4 |
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"]; NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"}; NSNumber *shouldUseLiterals = @YES; NSNumber *buildingStreetNumber = @10018; |
不應該:
1 2 3 4 |
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]; |
常量
常量是容易重複被使用和無需通過查詢和代替就能快速修改值。常量應該使用static
來宣告而不是使用#define
,除非顯式地使用巨集。
應該:
1 2 3 |
static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com"; static CGFloat const RWTImageThumbnailHeight = 50.0; |
不應該:
1 2 3 |
#define CompanyName @"RayWenderlich.com" #define thumbnailHeight 2 |
列舉型別
當使用enum
時,推薦使用新的固定基本型別規格,因為它有更強的型別檢查和程式碼補全。現在SDK有一個巨集NS_ENUM()
來幫助和鼓勵你使用固定的基本型別。
例如:
1 2 3 4 5 |
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) { RWTLeftMenuTopItemMain, RWTLeftMenuTopItemShows, RWTLeftMenuTopItemSchedule }; |
你也可以顯式地賦值(展示舊的k-style常量定義):
1 2 3 4 5 6 |
typedef NS_ENUM(NSInteger, RWTGlobalConstants) { RWTPinSizeMin = 1, RWTPinSizeMax = 5, RWTPinCountMin = 100, RWTPinCountMax = 500, }; |
舊的k-style常量定義應該避免除非編寫Core Foundation C的程式碼。
不應該:
1 2 3 4 |
enum GlobalConstants { kMaxPinSize = 5, kMaxPinCount = 500, }; |
Case語句
大括號在case語句中並不是必須的,除非編譯器強制要求。當一個case語句包含多行程式碼時,大括號應該加上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
switch (condition) { case 1: // ... break; case 2: { // ... // Multi-line example using braces break; } case 3: // ... break; default: // ... break; } |
有很多次,當相同程式碼被多個cases使用時,一個fall-through應該被使用。一個fall-through就是在case最後移除’break’語句,這樣就能夠允許執行流程跳轉到下一個case值。為了程式碼更加清晰,一個fall-through需要註釋一下。
1 2 3 4 5 6 7 8 9 10 |
switch (condition) { case 1: // ** fall-through! ** case 2: // code executed for values 1 and 2 break; default: // ... break; } |
當在switch使用列舉型別時,’default’是不需要的。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain; switch (menuType) { case RWTLeftMenuTopItemMain: // ... break; case RWTLeftMenuTopItemShows: // ... break; case RWTLeftMenuTopItemSchedule: // ... break; } |
私有屬性
私有屬性應該在類的實現檔案中的類擴充套件(匿名分類)中宣告,命名分類(比如RWTPrivate
或private
)應該從不使用除非是擴充套件其他類。匿名分類應該通過使用<headerfile>+Private.h檔案的命名規則暴露給測試。
例如:
1 2 3 4 5 6 7 |
@interface RWTDetailViewController () @property (strong, nonatomic) GADBannerView *googleAdView; @property (strong, nonatomic) ADBannerView *iAdView; @property (strong, nonatomic) UIWebView *adXWebView; @end |
布林值
Objective-C使用YES
和NO
。因為true
和false
應該只在CoreFoundation,C或C++程式碼使用。既然nil
解析成NO
,所以沒有必要在條件語句比較。不要拿某樣東西直接與YES
比較,因為YES
被定義為1和一個BOOL
能被設定為8位。
這是為了在不同檔案保持一致性和在視覺上更加簡潔而考慮。
應該:
1 2 |
if (someObject) {} if (![anotherObject boolValue]) {} |
不應該:
1 2 3 4 |
if (someObject == nil) {} if ([anotherObject boolValue] == NO) {} if (isAwesome == YES) {} // Never do this. if (isAwesome == true) {} // Never do this. |
如果BOOL
屬性的名字是一個形容詞,屬性就能忽略”is”字首,但要指定get訪問器的慣用名稱。例如:
1 |
@property (assign, getter=isEditable) BOOL editable; |
文字和例子從這裡引用Cocoa Naming Guidelines
條件語句
條件語句主體為了防止出錯應該使用大括號包圍,即使條件語句主體能夠不用大括號編寫(如,只用一行程式碼)。這些錯誤包括新增第二行程式碼和期望它成為if語句;還有,even more dangerous defect可能發生在if語句裡面一行程式碼被註釋了,然後下一行程式碼不知不覺地成為if語句的一部分。除此之外,這種風格與其他條件語句的風格保持一致,所以更加容易閱讀。
應該:
1 2 3 |
if (!error) { return success; } |
不應該:
1 2 |
if (!error) return success; |
或
1 |
if (!error) return success; |
三元操作符
當需要提高程式碼的清晰性和簡潔性時,三元操作符?:
才會使用。單個條件求值常常需要它。多個條件求值時,如果使用if
語句或重構成例項變數時,程式碼會更加易讀。一般來說,最好使用三元操作符是在根據條件來賦值的情況下。
Non-boolean的變數與某東西比較,加上括號()會提高可讀性。如果被比較的變數是boolean型別,那麼就不需要括號。
應該:
1 2 3 4 5 |
NSInteger value = 5; result = (value != 0) ? x : y; BOOL isHorizontal = YES; result = isHorizontal ? x : y; |
不應該:
1 |
result = a > b ? x = c > d ? c : d : y; |
Init方法
Init方法應該遵循Apple生成程式碼模板的命名規則。返回型別應該使用instancetype
而不是id
1 2 3 4 5 6 7 |
- (instancetype)init { self = [super init]; if (self) { // ... } return self; } |
檢視關於instancetype的文章Class Constructor Methods
類構造方法
當類構造方法被使用時,它應該返回型別是instancetype
而不是id
。這樣確保編譯器正確地推斷結果型別。
1 2 3 |
@interface Airplane + (instancetype)airplaneWithType:(RWTAirplaneType)type; @end |
關於更多instancetype資訊,請檢視NSHipster.com
CGRect函式
當訪問CGRect
裡的x
, y
, width
, 或 height
時,應該使用CGGeometry
函式而不是直接通過結構體來訪問。引用Apple的CGGeometry
:
在這個參考文件中所有的函式,接受CGRect結構體作為輸入,在計算它們結果時隱式地標準化這些rectangles。因此,你的應用程式應該避免直接訪問和修改儲存在CGRect資料結構中的資料。相反,使用這些函式來操縱rectangles和獲取它們的特性。
應該:
1 2 3 4 5 6 7 |
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.0, 0.0, width, height); |
不應該:
1 2 3 4 5 6 7 |
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” 或 “happy”路徑。也就是不要巢狀if
語句,多個返回語句也是OK。
應該:
1 2 3 4 5 6 7 |
- (void)someMethod { if (![someOther boolValue]) { return; } //Do something important } |
不應該:
1 2 3 4 5 |
- (void)someMethod { if ([someOther boolValue]) { //Do something important } } |
錯誤處理
當方法通過引用來返回一個錯誤引數,判斷返回值而不是錯誤變數。
應該:
1 2 3 4 |
NSError *error; if (![self trySomethingWithError:&error]) { // Handle Error } |
不應該:
1 2 3 4 5 |
NSError *error; [self trySomethingWithError:&error]; if (error) { // Handle Error } |
在成功的情況下,有些Apple的APIs記錄垃圾值(garbage values)到錯誤引數(如果non-NULL),那麼判斷錯誤值會導致false負值和crash。
單例模式
單例物件應該使用執行緒安全模式來建立共享例項。
1 2 3 4 5 6 7 8 9 10 |
+ (instancetype)sharedInstance { static id sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } |
這會防止possible and sometimes prolific crashes.
換行符
換行符是一個很重要的主題,因為它的風格指南主要為了列印和網上的可讀性。
例如:
1 |
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]; |
一行很長的程式碼應該分成兩行程式碼,下一行用兩個空格隔開。
1 2 |
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]; |
Xcode工程
物理檔案應該與Xcode工程檔案保持同步來避免檔案擴張。任何Xcode分組的建立應該在檔案系統的檔案體現。程式碼不僅是根據型別來分組,而且還可以根據功能來分組,這樣程式碼更加清晰。
儘可能在target的Build Settings開啟”Treat Warnings as Errors,和啟用以下additional warnings。如果你需要忽略特殊的警告,使用 Clang’s pragma feature。
其他Objective-C編碼規範
如果我們的編碼規範不符合你的口味,可以檢視其他的編碼規範: