原文 https://github.com/objc-zen/objc-zen-book
譯文 https://github.com/oa414/objc-zen-book-cn

前言
我們?cè)?2013 年 11 月份開(kāi)始寫(xiě)這本書(shū),最初的目標(biāo)是提供一份如何編寫(xiě)干凈漂亮的 Objective-C 代碼的指南:現(xiàn)在雖然有很多指南,但是它們都是有一些問(wèn)題的。我們不想介紹一些死板的規(guī)定,我們想提供一個(gè)在開(kāi)發(fā)者們之間寫(xiě)更一致的代碼的途徑。隨時(shí)間的推移,這本書(shū)開(kāi)始轉(zhuǎn)向介紹如何設(shè)計(jì)和構(gòu)建優(yōu)秀的代碼。
這本書(shū)的觀點(diǎn)是代碼不僅是可以編譯的,同時(shí)應(yīng)該是 “有效” 的。好的代碼有一些特性:簡(jiǎn)明,自我解釋?zhuān)瑑?yōu)秀的組織,良好的文檔,良好的命名,優(yōu)秀的設(shè)計(jì)以及可以被久經(jīng)考驗(yàn)。
本書(shū)的一個(gè)理念是是代碼的清晰性優(yōu)先于性能,同時(shí)闡述為什么應(yīng)該這么做。
雖然所有的代碼都是 Objective-C 寫(xiě)的,但是一些主題是通用的,并且獨(dú)立于編程語(yǔ)言。
Swift
在 2014 年 6 月 6 日,蘋(píng)果發(fā)布了面向 iOS 和 Mac 開(kāi)發(fā)的新語(yǔ)言: Swift。
這個(gè)新語(yǔ)言與 Objective-C 截然不同。所以,我們改變了寫(xiě)這本書(shū)的計(jì)劃。我們決定發(fā)布這本書(shū)當(dāng)前的狀態(tài),而不是繼續(xù)書(shū)寫(xiě)我們?cè)瓉?lái)計(jì)劃寫(xiě)下去的主題。
Objective-C 沒(méi)有消失,但是現(xiàn)在用一個(gè)慢慢失去關(guān)注的語(yǔ)言來(lái)繼續(xù)寫(xiě)這本書(shū)并不是一個(gè)明智的選擇。
貢獻(xiàn)給社區(qū)
我們將這本書(shū)免費(fèi)發(fā)布并且貢獻(xiàn)給社區(qū),因?yàn)槲覀兿M峁┙o讀者一些有價(jià)值的內(nèi)容。如果你能學(xué)到至少一條最佳實(shí)踐,我們的目的就達(dá)到了。
我們已經(jīng)非常用心地打磨了這些文字,但是仍然可能有一些拼寫(xiě)或者其他錯(cuò)誤。我們非常希望讀者給我們一個(gè)反饋或者建議,以來(lái)改善本書(shū)。所以如果有什么問(wèn)題的話,請(qǐng)聯(lián)系我們。我們非常歡迎各種 pull-request。
作者
Luca Bernardi
- http://lucabernardi.com
- @luka_bernardi
- http://github.com/lukabernardi
Alberto De Bortoli
關(guān)于中文翻譯
譯者
林翔宇
龐博
翻譯已得到原作者許可,并且會(huì)在更加完善后申請(qǐng)合并到原文倉(cāng)庫(kù)。
部分譯文表達(dá)可能存在不妥之處,非常歡迎各種修訂建議和校隊(duì)。 請(qǐng)直接 fork 本倉(cāng)庫(kù),在 README.md 文件中修改,并申請(qǐng) pull request 到 https://github.com/oa414/objc-zen-book-cn/。
條件語(yǔ)句
為了避免錯(cuò)誤,條件語(yǔ)句體應(yīng)該總是被大括號(hào)包圍,即使可以不這樣做(比如,條件語(yǔ)句體只有一行內(nèi)容)。可能的錯(cuò)誤是:多加了第二行,并且誤以為它是 if 語(yǔ)句體里面的。此外,更危險(xiǎn)的是,如果把 if 語(yǔ)句體里的一行注釋掉了,之后的一行代碼會(huì)成為 if 語(yǔ)句里的代碼。
推薦:
if (!error) {
return success;
}
不推薦:
if (!error)
return success;
或者
if (!error) return success;
在 2014年2月 蘋(píng)果的 SSL/TLS 實(shí)現(xiàn)里面發(fā)現(xiàn)了知名的 goto fail 錯(cuò)誤。
代碼在這里:
static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
...
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
...
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
顯而易見(jiàn),這里有沒(méi)有括號(hào)包圍的2行連續(xù)的 goto fail;
。我們當(dāng)然不希望寫(xiě)出上面的代碼導(dǎo)致錯(cuò)誤。
此外,在其他條件語(yǔ)句里面也應(yīng)該按照這種風(fēng)格統(tǒng)一,這樣更便于檢查。
尤達(dá)表達(dá)式
不要使用尤達(dá)表達(dá)式。尤達(dá)表達(dá)式是指,拿一個(gè)常量去和變量比較而不是拿變量去和常量比較。它就像是在表達(dá) “藍(lán)色是不是天空的顏色” 或者 “高個(gè)是不是這個(gè)男人的屬性” 而不是 “天空是不是藍(lán)的” 或者 “這個(gè)男人是不是高個(gè)子的”

(譯者注:名字起源于星球大戰(zhàn)中尤達(dá)大師的講話方式,總是用倒裝的語(yǔ)序)
推薦:
if ([myValue isEqual:@42]) { ...
不推薦:
if ([@42 isEqual:myValue]) { ...
nil 和 BOOL 檢查
類(lèi)似于 Yoda 表達(dá)式,nil 檢查的方式也是存在爭(zhēng)議的。一些 notous 庫(kù)像這樣檢查對(duì)象是否為 nil:
if (nil == myValue) { ...
或許有人會(huì)提出這是錯(cuò)的,因?yàn)樵?nil 作為一個(gè)常量的情況下,這樣做就像 Yoda 表達(dá)式了。 但是一些程序員這么做的原因是為了避免調(diào)試的困難,看下面的代碼:
if (myValue == nil) { ...
如果程序員敲錯(cuò)成這樣:
if (myValue = nil) { ...
這是合法的語(yǔ)句,但是即使你是一個(gè)豐富經(jīng)驗(yàn)的程序員,即使盯著眼睛瞧上好多遍也很難調(diào)試出錯(cuò)誤。但是如果把 nil 放在左邊,因?yàn)樗荒鼙毁x值,所以就不會(huì)發(fā)生這樣的錯(cuò)誤。 如果程序員這樣做,他/她就可以輕松檢查出可能的原因,比一遍遍檢查敲下的代碼要好很多。
為了避免這些奇怪的問(wèn)題,可以用感嘆號(hào)來(lái)作為運(yùn)算符。因?yàn)?nil 是 解釋到 NO,所以沒(méi)必要在條件語(yǔ)句里面把它和其他值比較。同時(shí),不要直接把它和 YES
比較,因?yàn)?YES
的定義是 1, 而 BOOL
是 8 bit的,實(shí)際上是 char 類(lèi)型。
推薦:
if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...
不推薦:
if (someObject == YES) { ... // Wrong
if (myRawValue == YES) { ... // Never do this.
if ([someObject boolValue] == NO) { ...
同時(shí)這樣也能提高一致性,以及提升可讀性。
黃金大道
當(dāng)編寫(xiě)條件語(yǔ)句的時(shí)候,左邊的代碼間距應(yīng)該是一個(gè)“黃金”或者“快樂(lè)”的大道。 這是說(shuō),不要嵌套 if
語(yǔ)句。多個(gè) return 語(yǔ)句是 OK 的。這樣可以避免 Cyclomatic 復(fù)雜性 (譯者注: https://en.wikipedia.org/wiki/Cyclomatic_complexity),并且讓代碼更加容易閱讀。因?yàn)槟愕姆椒ǖ闹匾糠譀](méi)有嵌套在分支上,你可以很清楚地找到相關(guān)的代碼。
推薦:
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
不推薦:
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}
復(fù)雜的表達(dá)式
當(dāng)你有一個(gè)復(fù)雜的 if 子句的時(shí)候,你應(yīng)該把它們提取出來(lái)賦給一個(gè) BOOL 變量,這樣可以讓邏輯更清楚,而且讓每個(gè)子句的意義體現(xiàn)出來(lái)。
BOOL nameContainsSwift = [sessionName containsString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession = nameContainsSwift && isCurrentYear;
if (isSwiftSession) {
// Do something very cool
}
三元運(yùn)算符
三元運(yùn)算符 ? 應(yīng)該只用在它能讓代碼更加清楚的地方。 一個(gè)條件語(yǔ)句的所有的變量應(yīng)該是已經(jīng)被求值了的。類(lèi)似 if 語(yǔ)句,計(jì)算多個(gè)條件子句通常會(huì)讓語(yǔ)句更加難以理解?;蛘呖梢园阉鼈冎貥?gòu)到實(shí)例變量里面。
推薦:
result = a > b ? x : y;
不推薦:
result = a > b ? x = c > d ? c : d : y;
當(dāng)三元運(yùn)算符的第二個(gè)參數(shù)(if 分支)返回和條件語(yǔ)句中已經(jīng)檢查的對(duì)象一樣的對(duì)象的時(shí)候,下面的表達(dá)方式更靈巧:
推薦:
result = object ? : [self createObject];
不推薦:
result = object ? object : [self createObject];
錯(cuò)誤處理
當(dāng)方法返回一個(gè)錯(cuò)誤參數(shù)的引用的時(shí)候,檢查返回值,而不是錯(cuò)誤的變量。
推薦:
NSError *error = nil;
if (![self trySomethingWithError:&error]) {
// Handle Error
}
此外,一些蘋(píng)果的 API 在成功的情況下會(huì)對(duì) error 參數(shù)(如果它非 NULL)寫(xiě)入垃圾值(garbage values),所以如果檢查 error 的值可能導(dǎo)致錯(cuò)誤 (甚至崩潰)。
Case語(yǔ)句
除非編譯器強(qiáng)制要求,括號(hào)在 case 語(yǔ)句里面是不必要的。但是當(dāng)一個(gè) case 包含了多行語(yǔ)句的時(shí)候,需要加上括號(hào)。
switch (condition) {
case 1:
// ...
break;
case 2: {
// ...
// Multi-line example using braces
break;
}
case 3:
// ...
break;
default:
// ...
break;
}
有時(shí)候可以使用 fall-through 在不同的 case 里面執(zhí)行同一段代碼。一個(gè) fall-through 是指移除 case 語(yǔ)句的 “break” 然后讓下面的 case 繼續(xù)執(zhí)行。
switch (condition) {
case 1:
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
當(dāng)在 switch 語(yǔ)句里面使用一個(gè)可枚舉的變量的時(shí)候,default
是不必要的。比如:
switch (menuType) {
case ZOCEnumNone:
// ...
break;
case ZOCEnumValue1:
// ...
break;
case ZOCEnumValue2:
// ...
break;
}
此外,為了避免使用默認(rèn)的 case,如果新的值加入到 enum,程序員會(huì)馬上收到一個(gè) warning 通知
Enumeration value 'ZOCEnumValue3' not handled in switch.(枚舉類(lèi)型 'ZOCEnumValue3' 沒(méi)有被 switch 處理)
枚舉類(lèi)型
當(dāng)使用 enum
的時(shí)候,建議使用新的固定的基礎(chǔ)類(lèi)型定義,因它有更強(qiáng)大的的類(lèi)型檢查和代碼補(bǔ)全。 SDK 現(xiàn)在有一個(gè) 宏來(lái)鼓勵(lì)和促進(jìn)使用固定類(lèi)型定義 - NS_ENUM()
**例子: **
typedef NS_ENUM(NSUInteger, ZOCMachineState) {
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
};
命名
通用的約定
盡可能遵守 Apple 的命名約定,尤其是和 內(nèi)存管理規(guī)則 (NARC) 相關(guān)的地方。
推薦使用長(zhǎng)的、描述性的方法和變量名
推薦:
UIButton *settingsButton;
不推薦:
UIButton *setBut;
常量
常量應(yīng)該使用駝峰命名法,并且為了清楚,應(yīng)該用相關(guān)的類(lèi)名作為前綴。
推薦:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
不推薦:
static const NSTimeInterval fadeOutTime = 0.4;
常量應(yīng)該盡量使用一致的字符串字面值或者數(shù)字,這樣便于經(jīng)常用到的時(shí)候復(fù)用,并且可以快速修改而避免查找和替換。 常量應(yīng)該用 static
聲明,不要使用 #define
,除非它就是明確作為一個(gè)宏來(lái)用的。
推薦:
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推薦:
#define CompanyName @"Apple Inc."
#define magicNumber 42
常量應(yīng)該在 interface 文件中這樣被聲明:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并且應(yīng)該在實(shí)現(xiàn)文件中實(shí)現(xiàn)它的定義。
你只需要為公開(kāi)的常量添加命名空間前綴。即使私有常量在實(shí)現(xiàn)文件中可能以不同的模式使用,你也不需要堅(jiān)持這個(gè)規(guī)則了。
方法
對(duì)于方法簽名,在方法類(lèi)型 (-
/+
符號(hào))后應(yīng)該要有一個(gè)空格。方法段之間也應(yīng)該有一個(gè)空格(來(lái)符合 Apple 的規(guī)范)。在參數(shù)名稱(chēng)之前總是應(yīng)該有一個(gè)描述性的關(guān)鍵詞。
使用“and”命名的時(shí)候應(yīng)當(dāng)更加謹(jǐn)慎。它不應(yīng)該用作闡明有多個(gè)參數(shù),比如下面的initWithWidth:height:
例子:
推薦:
- (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.
字面值
NSString
, NSDictionary
, NSArray
, 和 NSNumber
字面值應(yīng)該用在任何創(chuàng)建不可變的實(shí)例對(duì)象。特別小心不要把 nil
放進(jìn) NSArray
和 NSDictionary
里,這會(huì)導(dǎo)致崩潰
例子:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @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 *buildingZIPCode = [NSNumber numberWithInteger:10018];
對(duì)于那些可變的副本,我們推薦使用明確的如 NSMutableArray
, NSMutableString
這些類(lèi)。
下面的例子應(yīng)該被避免:
NSMutableArray *aMutableArray = [@[] mutableCopy];
上面的書(shū)寫(xiě)方式存在效率以及可讀性的問(wèn)題。效率方面,一個(gè)不必要的不可變變量被創(chuàng)建,并且馬上被廢棄了;這并不會(huì)讓你的 App 變得更慢(除非這個(gè)方法會(huì)被很頻繁地調(diào)用),但是確實(shí)沒(méi)必要為了少打幾個(gè)字而這樣做。對(duì)于可讀性來(lái)說(shuō),存在兩個(gè)問(wèn)題:第一個(gè)是當(dāng)瀏覽代碼并且看見(jiàn) @[]
的時(shí)候你的腦海里馬上會(huì)聯(lián)系到 NSArray
的實(shí)例,但是在這種情形下你需要停下來(lái)思考下。另一個(gè)方面,一些新手看到后可能會(huì)對(duì)可變和不可變對(duì)象的分歧感到不舒服。他/她可能對(duì)創(chuàng)造一個(gè)可變對(duì)象的副本不是很熟悉(當(dāng)然這并不是說(shuō)這個(gè)知識(shí)不重要)。當(dāng)然,這并不是說(shuō)存在絕對(duì)的錯(cuò)誤,只是可用性(包括可讀性)有一些問(wèn)題。