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

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

前言

我們在 2013 年 11 月份開始寫這本書,最初的目標是提供一份如何編寫干凈漂亮的 Objective-C 代碼的指南:現(xiàn)在雖然有很多指南,但是它們都是有一些問題的。我們不想介紹一些死板的規(guī)定,我們想提供一個在開發(fā)者們之間寫更一致的代碼的途徑。隨時間的推移,這本書開始轉(zhuǎn)向介紹如何設(shè)計和構(gòu)建優(yōu)秀的代碼。

這本書的觀點是代碼不僅是可以編譯的,同時應(yīng)該是 “有效” 的。好的代碼有一些特性:簡明,自我解釋,優(yōu)秀的組織,良好的文檔,良好的命名,優(yōu)秀的設(shè)計以及可以被久經(jīng)考驗。
本書的一個理念是是代碼的清晰性優(yōu)先于性能,同時闡述為什么應(yīng)該這么做。
雖然所有的代碼都是 Objective-C 寫的,但是一些主題是通用的,并且獨立于編程語言。

Swift

在 2014 年 6 月 6 日,蘋果發(fā)布了面向 iOS 和 Mac 開發(fā)的新語言: Swift。
這個新語言與 Objective-C 截然不同。所以,我們改變了寫這本書的計劃。我們決定發(fā)布這本書當前的狀態(tài),而不是繼續(xù)書寫我們原來計劃寫下去的主題。
Objective-C 沒有消失,但是現(xiàn)在用一個慢慢失去關(guān)注的語言來繼續(xù)寫這本書并不是一個明智的選擇。

貢獻給社區(qū)

我們將這本書免費發(fā)布并且貢獻給社區(qū),因為我們希望提供給讀者一些有價值的內(nèi)容。如果你能學到至少一條最佳實踐,我們的目的就達到了。

我們已經(jīng)非常用心地打磨了這些文字,但是仍然可能有一些拼寫或者其他錯誤。我們非常希望讀者給我們一個反饋或者建議,以來改善本書。所以如果有什么問題的話,請聯(lián)系我們。我們非常歡迎各種 pull-request。

作者

Luca Bernardi

Alberto De Bortoli

關(guān)于中文翻譯

譯者

林翔宇

龐博

翻譯已得到原作者許可,并且會在更加完善后申請合并到原文倉庫。

部分譯文表達可能存在不妥之處,非常歡迎各種修訂建議和校隊。 請直接 fork 本倉庫,在 README.md 文件中修改,并申請 pull request 到 https://github.com/oa414/objc-zen-book-cn/

條件語句

為了避免錯誤,條件語句體應(yīng)該總是被大括號包圍,即使可以不這樣做(比如,條件語句體只有一行內(nèi)容)??赡艿腻e誤是:多加了第二行,并且誤以為它是 if 語句體里面的。此外,更危險的是,如果把 if 語句體里的一行注釋掉了,之后的一行代碼會成為 if 語句里的代碼。

推薦:

if (!error) {
    return success;
}

不推薦:

if (!error)
    return success;

或者

if (!error) return success;

在 2014年2月 蘋果的 SSL/TLS 實現(xiàn)里面發(fā)現(xiàn)了知名的 goto fail 錯誤。

代碼在這里:

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;
}

顯而易見,這里有沒有括號包圍的2行連續(xù)的 goto fail; 。我們當然不希望寫出上面的代碼導(dǎo)致錯誤。

此外,在其他條件語句里面也應(yīng)該按照這種風格統(tǒng)一,這樣更便于檢查。

尤達表達式

不要使用尤達表達式。尤達表達式是指,拿一個常量去和變量比較而不是拿變量去和常量比較。它就像是在表達 “藍色是不是天空的顏色” 或者 “高個是不是這個男人的屬性” 而不是 “天空是不是藍的” 或者 “這個男人是不是高個子的”

Yoda
Yoda

(譯者注:名字起源于星球大戰(zhàn)中尤達大師的講話方式,總是用倒裝的語序)

推薦:

if ([myValue isEqual:@42]) { ...

不推薦:

if ([@42 isEqual:myValue]) { ...

nil 和 BOOL 檢查

類似于 Yoda 表達式,nil 檢查的方式也是存在爭議的。一些 notous 庫像這樣檢查對象是否為 nil:

if (nil == myValue) { ...

或許有人會提出這是錯的,因為在 nil 作為一個常量的情況下,這樣做就像 Yoda 表達式了。 但是一些程序員這么做的原因是為了避免調(diào)試的困難,看下面的代碼:

if (myValue == nil) { ...

如果程序員敲錯成這樣:

if (myValue = nil) { ...

這是合法的語句,但是即使你是一個豐富經(jīng)驗的程序員,即使盯著眼睛瞧上好多遍也很難調(diào)試出錯誤。但是如果把 nil 放在左邊,因為它不能被賦值,所以就不會發(fā)生這樣的錯誤。 如果程序員這樣做,他/她就可以輕松檢查出可能的原因,比一遍遍檢查敲下的代碼要好很多。

為了避免這些奇怪的問題,可以用感嘆號來作為運算符。因為 nil 是 解釋到 NO,所以沒必要在條件語句里面把它和其他值比較。同時,不要直接把它和 YES 比較,因為 YES 的定義是 1, 而 BOOL 是 8 bit的,實際上是 char 類型。

推薦:

if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...

不推薦:

if (someObject == YES) { ... // Wrong
if (myRawValue == YES) { ... // Never do this.
if ([someObject boolValue] == NO) { ...

同時這樣也能提高一致性,以及提升可讀性。

黃金大道

當編寫條件語句的時候,左邊的代碼間距應(yīng)該是一個“黃金”或者“快樂”的大道。 這是說,不要嵌套 if 語句。多個 return 語句是 OK 的。這樣可以避免 Cyclomatic 復(fù)雜性 (譯者注: https://en.wikipedia.org/wiki/Cyclomatic_complexity),并且讓代碼更加容易閱讀。因為你的方法的重要部分沒有嵌套在分支上,你可以很清楚地找到相關(guān)的代碼。

推薦:

- (void)someMethod {
  if (![someOther boolValue]) {
      return;
  }

  //Do something important
}

不推薦:

- (void)someMethod {
  if ([someOther boolValue]) {
    //Do something important
  }
}

復(fù)雜的表達式

當你有一個復(fù)雜的 if 子句的時候,你應(yīng)該把它們提取出來賦給一個 BOOL 變量,這樣可以讓邏輯更清楚,而且讓每個子句的意義體現(xiàn)出來。

BOOL nameContainsSwift  = [sessionName containsString:@"Swift"];
BOOL isCurrentYear      = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession     = nameContainsSwift && isCurrentYear;

if (isSwiftSession) {
    // Do something very cool
}

三元運算符

三元運算符 ? 應(yīng)該只用在它能讓代碼更加清楚的地方。 一個條件語句的所有的變量應(yīng)該是已經(jīng)被求值了的。類似 if 語句,計算多個條件子句通常會讓語句更加難以理解?;蛘呖梢园阉鼈冎貥?gòu)到實例變量里面。

推薦:

result = a > b ? x : y;

不推薦:

result = a > b ? x = c > d ? c : d : y;

當三元運算符的第二個參數(shù)(if 分支)返回和條件語句中已經(jīng)檢查的對象一樣的對象的時候,下面的表達方式更靈巧:

推薦:

result = object ? : [self createObject];

不推薦:

result = object ? object : [self createObject];

錯誤處理

當方法返回一個錯誤參數(shù)的引用的時候,檢查返回值,而不是錯誤的變量。

推薦:

NSError *error = nil;
if (![self trySomethingWithError:&error]) {
    // Handle Error
}

此外,一些蘋果的 API 在成功的情況下會對 error 參數(shù)(如果它非 NULL)寫入垃圾值(garbage values),所以如果檢查 error 的值可能導(dǎo)致錯誤 (甚至崩潰)。

Case語句

除非編譯器強制要求,括號在 case 語句里面是不必要的。但是當一個 case 包含了多行語句的時候,需要加上括號。

switch (condition) {
    case 1:
        // ...
        break;
    case 2: {
        // ...
        // Multi-line example using braces
        break;
       }
    case 3:
        // ...
        break;
    default: 
        // ...
        break;
}

有時候可以使用 fall-through 在不同的 case 里面執(zhí)行同一段代碼。一個 fall-through 是指移除 case 語句的 “break” 然后讓下面的 case 繼續(xù)執(zhí)行。

switch (condition) {
    case 1:
    case 2:
        // code executed for values 1 and 2
        break;
    default: 
        // ...
        break;
}

當在 switch 語句里面使用一個可枚舉的變量的時候,default 是不必要的。比如:

switch (menuType) {
    case ZOCEnumNone:
        // ...
        break;
    case ZOCEnumValue1:
        // ...
        break;
    case ZOCEnumValue2:
        // ...
        break;
}

此外,為了避免使用默認的 case,如果新的值加入到 enum,程序員會馬上收到一個 warning 通知

Enumeration value 'ZOCEnumValue3' not handled in switch.(枚舉類型 'ZOCEnumValue3' 沒有被 switch 處理)

枚舉類型

當使用 enum 的時候,建議使用新的固定的基礎(chǔ)類型定義,因它有更強大的的類型檢查和代碼補全。 SDK 現(xiàn)在有一個 宏來鼓勵和促進使用固定類型定義 - NS_ENUM()

**例子: **

typedef NS_ENUM(NSUInteger, ZOCMachineState) {
    ZOCMachineStateNone,
    ZOCMachineStateIdle,
    ZOCMachineStateRunning,
    ZOCMachineStatePaused
};

命名

通用的約定

盡可能遵守 Apple 的命名約定,尤其是和 內(nèi)存管理規(guī)則 (NARC) 相關(guān)的地方。

推薦使用長的、描述性的方法和變量名

推薦:

UIButton *settingsButton;

不推薦:

UIButton *setBut;

常量

常量應(yīng)該使用駝峰命名法,并且為了清楚,應(yīng)該用相關(guān)的類名作為前綴。

推薦:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推薦:

static const NSTimeInterval fadeOutTime = 0.4;

常量應(yīng)該盡量使用一致的字符串字面值或者數(shù)字,這樣便于經(jīng)常用到的時候復(fù)用,并且可以快速修改而避免查找和替換。 常量應(yīng)該用 static 聲明,不要使用 #define,除非它就是明確作為一個宏來用的。

推薦:

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)該在實現(xiàn)文件中實現(xiàn)它的定義。

你只需要為公開的常量添加命名空間前綴。即使私有常量在實現(xiàn)文件中可能以不同的模式使用,你也不需要堅持這個規(guī)則了。

方法

對于方法簽名,在方法類型 (-/+ 符號)后應(yīng)該要有一個空格。方法段之間也應(yīng)該有一個空格(來符合 Apple 的規(guī)范)。在參數(shù)名稱之前總是應(yīng)該有一個描述性的關(guān)鍵詞。

使用“and”命名的時候應(yīng)當更加謹慎。它不應(yīng)該用作闡明有多個參數(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)建不可變的實例對象。特別小心不要把 nil 放進 NSArrayNSDictionary 里,這會導(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];

對于那些可變的副本,我們推薦使用明確的如 NSMutableArray, NSMutableString 這些類。

下面的例子應(yīng)該被避免:

NSMutableArray *aMutableArray = [@[] mutableCopy];

上面的書寫方式存在效率以及可讀性的問題。效率方面,一個不必要的不可變變量被創(chuàng)建,并且馬上被廢棄了;這并不會讓你的 App 變得更慢(除非這個方法會被很頻繁地調(diào)用),但是確實沒必要為了少打幾個字而這樣做。對于可讀性來說,存在兩個問題:第一個是當瀏覽代碼并且看見 @[] 的時候你的腦海里馬上會聯(lián)系到 NSArray 的實例,但是在這種情形下你需要停下來思考下。另一個方面,一些新手看到后可能會對可變和不可變對象的分歧感到不舒服。他/她可能對創(chuàng)造一個可變對象的副本不是很熟悉(當然這并不是說這個知識不重要)。當然,這并不是說存在絕對的錯誤,只是可用性(包括可讀性)有一些問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容