本文轉自:Objective-C 編碼風格指南 | www.samirchen.com
背景
保證自己的代碼遵循團隊統一的編碼規范是一個碼農的基本節操,能夠進入一個有統一編碼規范的團隊則是一個碼農的福氣。
本文主要是對以下幾個編碼規范的整理:
這里有些關于編碼風格 Apple 官方文檔,如果有些東西沒有提及,可以在以下文檔來查找更多細節:
- The Objective-C Programming Language
- Cocoa Fundamentals Guide
- Coding Guidelines for Cocoa
- iOS App Programming Guide
語言
使用美式英語。別用拼音。
推薦:
UIColor *myColor = [UIColor whiteColor];
不推薦:
UIColor *myColour = [UIColor whiteColor];
UIColor *woDeYanSe = [UIColor whiteColor];
代碼結構
使用 #pragma mark -
根據「代碼功能類別」、「protocol/delegate 方法實現」等依據對代碼進行分塊組織。代碼的組織順序從整體上盡量遵循我們的認知順序,組織規范如下:
// 先描述這個類是什么,它的屬性有什么。
// 每個屬性的 getter 方法在前,setter 方法在后。屬性的 getter/setter 方法的順序與屬性聲明順序一致。
#pragma mark - Property
- (id)customProperty {}
- (void)setCustomProperty:(id)value {}
// 再描述這個類的生命周期,從出生到消亡。
// 按照生命周期的順序來排序相關方法。
#pragma mark - Lifecycle
- (instancetype)init {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)viewDidAppear:(BOOL)animated {}
- (void)viewWillDisappear:(BOOL)animated {}
- (void)viewDidDisappear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
- (void)dealloc {}
// 如果這是一個 UIViewController 類,可以接著描述這個頁面可以跳轉到的其他頁面。
#pragma mark - Navigation
- (void)goToMainPage {}
- (void)goToUserPage {}
// 接著描述這個類的響應方法,能做哪些交互。
// 比如:按鈕點擊的響應方法、手勢的響應方法等等。
#pragma mark - Action
- (IBAction)submitData:(id)sender {}
// 然后描述這個類的其他分組方法。這里的分組可以是多個,如何分組可以由你擴展。
#pragma mark - <Other Functional Grouping>
- (void)someGroupedMethod {}
// 接下來描述這個類實現的 Protocol/Delegate 的方法。
// 先放自定義的 Protocol/Delegate 方法,后放官方提供的 Protocal/Delegate 方法。
#pragma mark - <Protocol/Delegate Conformance>
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
// 然后是對繼承的父類中方法重載。
// 先發自定義的父類方法重載,后方官方父類的方法重載。
#pragma mark - <Superclass Overridden>
- (void)someOverriddenMethod {}
#pragma mark - NSObject
- (NSString *)description {}
代碼如流水一樣,去敘述一個類。
空格
- 使用 Tab(4 個空格) 來做代碼縮進,不要用空格。
- 方法大括號和其他大括號(
if
/else
/switch
/while
等)總是在同一行打開,但在新的一行關閉。
推薦:
if (user.isHappy) {
// Do something
} else {
// Do something else
}
不推薦:
if (user.isHappy)
{
// Do something
}
else {
// Do something else
}
- 在兩個方法之間應該間隔一行且只有一行,這樣在視覺上更清晰。在方法內可以為分隔不同的功能代碼而空行,但通常都會把具有特定功能的代碼抽出來成為一個新方法。
- 優先使用 auto-synthesis。如果有必要
@synthesize
和@dynamic
的聲明應該在實現代碼中各占一行。 - 在調用方法時,避免以冒號對齊的格式來排版。因為有時前綴較長或者包含 Block 時會使得這種方式排版的代碼易讀性很差。
推薦:
// blocks are easily readable
[UIView animateWithDuration:1.0 animations:^{
// something
} completion:^(BOOL finished) {
// something
}];
不推薦:
// colon-aligning makes the block indentation hard to read
[UIView animateWithDuration:1.0
animations:^{
// something
}
completion:^(BOOL finished) {
// something
}];
注釋
當你寫代碼注釋時,需要注意你的注釋是解釋為什么要有這段代碼。一段注釋要確保跟代碼一致更新,否則就刪掉。
一般避免使用塊注釋,這樣占用空間太大,代碼應該盡量做到自解釋,代碼即注釋。當然,也有例外:你的注釋是為了生成文檔用。
命名
你可能是從 Java、Python、C++ 或是其他語言轉過來的,但是來到 Objective-C 這地盤,請遵守蘋果的命名規范,這樣你才能使得自己的代碼與周邊和諧統一,尤其需要注意 memory management rules (NARC) 相關的命名規范.
長的、描述性的方法和變量命名是好的,這使得代碼更容易被讀懂。
推薦:
UIButton *settingsButton;
不推薦:
UIButton *setBut;
在類名和常量名上應該使用兩個或三個字母的前綴(比如:CX、TB 等等)。但是在 Core Data 實體命名時應該省略前綴。
常量應該使用駝峰式命名規則,所有的單詞首字母大寫,并加上與類名有關的前綴。
推薦:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3;
不推薦:
static NSTimeInterval const fadetime = 1.7;
屬性也是使用駝峰式命名規則,但首單詞的首字母小寫。對屬性使用 auto-synthesis,而不是手動編寫 @synthesize 語句,除非你有一個好的理由。
推薦:
@property (strong, nonatomic) NSString *descriptiveVariableName;
不推薦:
id varnm;
下劃線
當使用屬性時,用 self.
來訪問,這就意味著所有的屬性都很有辨識度,因為他們前面有 self.
。
但是有 2 個特列:
- 在屬性的 getter/setter 方法中必須使用下劃線,(比如:_variableName)。
- 在類的初始化和銷毀方法中,有時為了避免屬性的 getter/setter 方法的副作用,可以使用下劃線。
局部變量不要包含下劃線。
方法
在方法簽名中,應該在方法類型(-/+ 符號)之后有一個空格。在方法各段之間應該也有一個空格(符合 Apple 的風格)。在參數之前應該包含一個描述性的關鍵字來描述參數。
and
這個詞的用法應該保留,它不應該用于多個參數之間。
推薦:
- (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;
不推薦:
-(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
,除了一些特別的情況。
應該使用私有屬性,而不要再使用實例變量了。這樣可以保持代碼的一致性。
除了在一些初始化方法(init
, initWithCoder:
, etc…)、銷毀方法(dealloc
)和自定義的 setters/getters 方法中外,不要直接使用下劃線的方式訪問實例變量。詳情參見這里。
推薦:
@interface RWTTutorial : NSObject
@property (strong, nonatomic) NSString *tutorialName;
@end
不推薦:
@interface RWTTutorial : NSObject {
NSString *tutorialName;
}
屬性特性
屬性特性的順序應該是:存儲特性、訪問特性、原子特性、getter/setter。其中存儲特性、原子特性應該顯式地列出來,有助于新手閱讀代碼。與在 Interface Builder 連接 UI 元素時自動生成代碼一致。
推薦:
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (strong, nonatomic) NSString *tutorialName;
@property (assign, readonly, nonatomic, getter=isFinished) BOOL finished;
不推薦:
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic) NSString *tutorialName;
具有值拷貝類型特定的屬性(如:NSString)應該優先使用 copy
而不是 strong
。這是因為即使你聲明一個 NSString
類型的屬性,有人也可能傳入一個 NSMutableString
的實例,然后在你沒有注意的情況下修改它。
推薦:
@property (copy, nonatomic) NSString *tutorialName;
不推薦:
@property (strong, nonatomic) NSString *tutorialName;
點符號語法
點符號語法是對方法調用語法很方便的一種封裝。在返回屬性時,使用點符號語法,屬性的 getter/setter 方法也能確保被調用。更多信息閱讀這里。
我們應該總是使用點符號語法來訪問或者修改屬性,因為它使得代碼更加簡潔。[]
則應該用在其他場景下。
推薦:
NSInteger arrayCount = self.array.count; // `count` is a property of NSArray.
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate; // `sharedApplication` is not a property of UIApplication.
不推薦:
NSInteger arrayCount = [self.array count]; // `count` is a property of NSArray.
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate; // `sharedApplication` is not a property of UIApplication.
字面值
在創建 NSString
、NSDictionary
、NSArray
和 NSNumber
對象時,應該使用字面值語法。尤其需要注意創建 NSArray
和 NSDictionary
對象時,不能傳入 nil
,否則會造成 crash。
推薦:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;
不推薦:
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];
常量
比起硬編碼字符串或數字的形式,我們應該常量來定義復用型變量,因為常量更容易被修改,而不需要我們 find + replace。使用常量時,我們應該使用 static
而不是 #define
一個類型不明的宏。
推薦:
static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
static CGFloat const RWTImageThumbnailHeight = 50.0;
不推薦:
#define CompanyName @"RayWenderlich.com"
#define thumbnailHeight 2
枚舉類型
當使用枚舉時,我們要用 NS_ENUM()
而不是 enum
。
例如:
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
你可以顯示的賦值:
typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
RWTPinSizeMin = 1,
RWTPinSizeMax = 5,
RWTPinCountMin = 100,
RWTPinCountMax = 500,
};
不推薦:
enum GlobalConstants {
kMaxPinSize = 5,
kMaxPinCount = 500,
};
Case 語句
除非編譯器強制要求,一般在 Case 語句中是不需要加括號的。當一個 Case 語句包含多行代碼,應該加上括號。
switch (condition) {
case 1:
// ...
break;
case 2: {
// ...
// Multi-line example using braces
break;
}
case 3:
// ...
break;
default:
// ...
break;
}
如果一段代碼被多個 Case 語句共享執行,那就要用 fall-through,即在 Case 語句中刪除 break
語句,讓代碼能夠執行到下一個 Case 中去,為了代碼清晰明了,用了 fall-through 時需要注釋一下。
switch (condition) {
case 1:
// ** fall-through! **
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
在 Swith 中使用枚舉類型時,是不需要 default
語句的,例如:
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
case RWTLeftMenuTopItemMain:
// ...
break;
case RWTLeftMenuTopItemShows:
// ...
break;
case RWTLeftMenuTopItemSchedule:
// ...
break;
}
私有屬性
私有屬性應該在類的實現文件(xxx.m)中的匿名擴展(Anonymous Category)中聲明。除非是要去擴展一個類,否則不要使用命名擴展(Named Category)。如果你要測試私有屬性,你可以通過 <headerfile>+Private.h
的方式把私有屬性暴露給測試人員。
For Example:
@interface RWTDetailViewController ()
@property (strong, nonatomic) GADBannerView *googleAdView;
@property (strong, nonatomic) ADBannerView *iAdView;
@property (strong, nonatomic) UIWebView *adXWebView;
@end
布爾值
Objective-C 使用 YES
和 NO
作為 BOOL 值。因此,true
和 false
只應該在 CoreFoundation、C、C++ 代碼中使用。由于 nil
會被解析為 NO
,所以沒有必要在條件語句中去比較它。 另外,永遠不要拿一個對象和 YES
比較,因為 YES
被定義為 1 并且 BOOL
值最多 8 bit。
這時為了在不同代碼中保持一致性和簡潔性。
推薦:
if (someObject) {}
if (![anotherObject boolValue]) {}
不推薦:
if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.
如果一個 BOOL
類型的屬性是形容詞,那么它的命名可以省略掉 “is” 前綴,但是我們還是需要給它指定慣用的 getter 方法名,例如:
@property (assign, getter=isEditable) BOOL editable;
更多內容詳見:Cocoa Naming Guidelines。
條件語句
條件語句應該使用大括號包圍,即使能夠不用時(比如條件代碼只有一行)也不要省略大括號,這樣可以最大可能的避免出錯(比如條件語句不小心被注釋了),同時也保持了大括號的使用風格一致。
推薦:
if (!error) {
return success;
}
不推薦:
if (!error)
return success;
或者
if (!error) return success;
三元操作符
只有在能提高代碼清晰性和可讀性的情況下,才應該使用三元操作符 ?:
。單個條件判斷時可以用到它,多個條件判斷時還是用 if
來提高代碼可讀性吧。一般來說,使用三元操作符最好的場景是根據條件來賦值的時候。
非布爾類型的變量與某對象比較時最好加上括號來提高代碼可讀性,如果被比較的變量是布爾類型那就不用括號了。
推薦:
NSInteger value = 5;
result = (value != 0) ? x : y;
BOOL isHorizontal = YES;
result = isHorizontal ? x : y;
不推薦:
result = a > b ? x = c > d ? c : d : y;
初始化方法
Init 方法應該遵循 Apple 生成代碼模板的命名規則。返回類型應該使用 instancetype
而不是 id
。
- (instancetype)init {
self = [super init];
if (self) {
// ...
}
return self;
}
類構造方法
當使用類構造方法時,應該返回的類型是 instancetype
而不是 id
。這樣確保編譯器正確地推斷結果類型。
@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end
查看更多關于 instancetype 的信息:NSHipster.com。
CGRect 方法
當訪問 CGRect 的 x
、y
、width
、height
屬性時,總是使用 CGGeometry
functions 相關的函數,而不是直接從結構體訪問。
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.
推薦:
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);
不推薦:
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
語句,即使寫多個 return
語句也是 OK 的。
推薦:
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
不推薦:
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}
單例
單例對象應該使用線程安全的方式來創建共享實例。
+ (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。
換行符
換行符主要是在提高打印和網上閱讀時的代碼可讀性時顯得很重要。
例如:
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
一行較長的代碼最好能換行再加一個 Tab。
self.productsRequest = [[SKProductsRequest alloc]
initWithProductIdentifiers:productIdentifiers];
Xcode 工程
物理文件應該與 Xcode 項目目錄保持同步來避免文件管理雜亂。創建任何 Xcode group 應該與文件系統中的文件夾保持映射。代碼分類除了以類型分類,從大的方面上也應該以功能分類。
如果可以的話,打開 Xcode 的 Treat Warnings as Errors
來降低對 warning 的容忍度。如果有時候確實要忽略某一個 warning,你可以使用 Clang's pragma feature。