禪與 Objective-C 編程藝術(shù)(上)

原文 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

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è)子的”

Yoda
Yoda

(譯者注:名字起源于星球大戰(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) NSArrayNSDictionary 里,這會(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)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容