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

禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)

前言

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

這本書的觀點(diǎn)是代碼不僅是可以編譯的,同時(shí)應(yīng)該是 “有效” 的碍拆。好的代碼有一些特性:簡明若治,自我解釋,優(yōu)秀的組織感混,良好的文檔端幼,良好的命名,優(yōu)秀的設(shè)計(jì)以及可以被久經(jīng)考驗(yàn)弧满。

本書的一個(gè)理念是是代碼的清晰性優(yōu)先于性能婆跑,同時(shí)闡述為什么應(yīng)該這么做。

雖然所有的代碼都是 Objective-C 寫的庭呜,但是一些主題是通用的滑进,并且獨(dú)立于編程語言。

Swift

在 2014 年 6 月 6 日募谎,蘋果發(fā)布了面向 iOS 和 Mac 開發(fā)的新語言: Swift扶关。
這個(gè)新語言與 Objective-C 截然不同。所以数冬,我們改變了寫這本書的計(jì)劃节槐。我們決定發(fā)布這本書當(dāng)前的狀態(tài),而不是繼續(xù)書寫我們原來計(jì)劃寫下去的主題拐纱。

Objective-C 沒有消失疯淫,但是現(xiàn)在用一個(gè)慢慢失去關(guān)注的語言來繼續(xù)寫這本書并不是一個(gè)明智的選擇。

貢獻(xiàn)給社區(qū)

我們將這本書免費(fèi)發(fā)布并且貢獻(xiàn)給社區(qū)戳玫,因?yàn)槲覀兿M峁┙o讀者一些有價(jià)值的內(nèi)容熙掺。如果你能學(xué)到至少一條最佳實(shí)踐,我們的目的就達(dá)到了咕宿。

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

作者

Luca Bernardi

Alberto De Bortoli

關(guān)于中文翻譯

譯者

林翔宇

龐博

Kevin.Xiao

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

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

條件語句

條件語句體應(yīng)該總是被大括號包圍。盡管有時(shí)候你可以不使用大括號(比如截珍,條件語句體只有一行內(nèi)容)攀甚,但是這樣做會帶來問題隱患。比如岗喉,增加一行代碼時(shí)秋度,你可能會誤以為它是 if 語句體里面的。此外钱床,更危險(xiǎn)的是静陈,如果把 if 后面的那行代碼注釋掉,之后的一行代碼會成為 if 語句里的代碼诞丽。

推薦:

if (!error) {
    return success;
}

不推薦:

if (!error)
    return success;

if (!error) return success;

在 2014年2月 蘋果的 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;
}

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

此外,在其他條件語句里面也應(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á)大師的講話方式,總是用倒裝的語序)

推薦:

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

不推薦:

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

nil 和 BOOL 檢查

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

if (nil == myValue) { ...

或許有人會提出這是錯(cuò)的,因?yàn)樵?nil 作為一個(gè)常量的情況下苫亦,這樣做就像 Yoda 表達(dá)式了毛肋。 但是一些程序員這么做的原因是為了避免調(diào)試的困難怨咪,看下面的代碼:

if (myValue == nil) { ...

如果程序員敲錯(cuò)成這樣:

if (myValue = nil) { ...

這是合法的語句,但是即使你是一個(gè)豐富經(jīng)驗(yàn)的程序員润匙,即使盯著眼睛瞧上好多遍也很難調(diào)試出錯(cuò)誤诗眨。但是如果把 nil 放在左邊,因?yàn)樗荒鼙毁x值孕讳,所以就不會發(fā)生這樣的錯(cuò)誤匠楚。 如果程序員這樣做,他/她就可以輕松檢查出可能的原因厂财,比一遍遍檢查敲下的代碼要好很多芋簿。

為了避免這些奇怪的問題,可以用感嘆號來作為運(yùn)算符蟀苛。因?yàn)?nil 是 解釋到 NO益咬,所以沒必要在條件語句里面把它和其他值比較逮诲。同時(shí)帜平,不要直接把它和 YES 比較,因?yàn)?YES 的定義是 1梅鹦, 而 BOOL 是 8 bit的裆甩,實(shí)際上是 char 類型。

推薦:

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

不推薦:

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

同時(shí)這樣也能提高一致性齐唆,以及提升可讀性嗤栓。

黃金大道

在使用條件語句編程時(shí),代碼的左邊距應(yīng)該是一條“黃金”或者“快樂”的大道箍邮。 也就是說茉帅,不要嵌套 if 語句。使用多個(gè) return 可以避免增加循環(huán)的復(fù)雜度锭弊,并提高代碼的可讀性堪澎。因?yàn)榉椒ǖ闹匾糠譀]有嵌套在分支里面,并且你可以很清楚地找到相關(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)該把它們提取出來賦給一個(gè) BOOL 變量,這樣可以讓邏輯更清楚剑鞍,而且讓每個(gè)子句的意義體現(xiàn)出來昨凡。

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īng)該是已經(jīng)被求值了的蚁署。類似 if 語句便脊,計(jì)算多個(gè)條件子句通常會讓語句更加難以理解」飧辏或者可以把它們重構(gòu)到實(shí)例變量里面就轧。

推薦:

result = a > b ? x : y;

不推薦:

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

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

推薦:

result = object ? : [self createObject];

不推薦:

result = object ? object : [self createObject];

錯(cuò)誤處理

有些方法通過參數(shù)返回 error 的引用,使用這樣的方法時(shí)應(yīng)當(dāng)檢查方法的返回值妒御,而非 error 的引用解愤。

推薦:

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

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

Case語句

除非編譯器強(qiáng)制要求,括號在 case 語句里面是不必要的惋啃。但是當(dāng)一個(gè) case 包含了多行語句的時(shí)候哼鬓,需要加上括號。

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 語句的 “break” 然后讓下面的 case 繼續(xù)執(zhí)行异希。

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

當(dāng)在 switch 語句里面使用一個(gè)可枚舉的變量的時(shí)候,default 是不必要的绒瘦。比如:

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

此外称簿,為了避免使用默認(rèn)的 case,如果新的值加入到 enum惰帽,程序員會馬上收到一個(gè) warning 通知

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

枚舉類型

當(dāng)使用 enum 的時(shí)候憨降,建議使用新的固定的基礎(chǔ)類型定義,因?yàn)樗懈鼜?qiáng)大的類型檢查和代碼補(bǔ)全该酗。 SDK 現(xiàn)在有一個(gè) 宏來鼓勵(lì)和促進(jì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)該以駝峰法命名,并以相關(guān)類名作為前綴爵嗅。

推薦:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推薦:

static const NSTimeInterval fadeOutTime = 0.4;

推薦使用常量來代替字符串字面值和數(shù)字娇澎,這樣能夠方便復(fù)用,而且可以快速修改而不需要查找和替換操骡。常量應(yīng)該用 static 聲明為靜態(tài)常量九火,而不要用 #define,除非它明確的作為一個(gè)宏來使用册招。

推薦:

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;

不推薦:

#define CompanyName @"Apple Inc."
#define magicNumber 42

常量應(yīng)該在頭文件中以這樣的形式暴露給外部:

extern NSString *const ZOCCacheControllerDidClearCacheNotification;

并在實(shí)現(xiàn)文件中為它賦值岔激。

只有公有的常量才需要添加命名空間作為前綴。盡管實(shí)現(xiàn)文件中私有常量的命名可以遵循另外一種模式是掰,你仍舊可以遵循這個(gè)規(guī)則虑鼎。

方法

方法名與方法類型 (-/+ 符號)之間應(yīng)該以空格間隔。方法段之間也應(yīng)該以空格間隔(以符合 Apple 風(fēng)格)。參數(shù)前應(yīng)該總是有一個(gè)描述性的關(guān)鍵詞炫彩。

盡可能少用 "and" 這個(gè)詞匾七。它不應(yīng)該用來闡明有多個(gè)參數(shù),比如下面的 initWithWidth:height: 這個(gè)例子:

推薦:

- (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.

字面值

使用字面值來創(chuàng)建不可變的 NSString, NSDictionary, NSArray, 和 NSNumber 對象江兢。注意不要將 nil 傳進(jìn) NSArrayNSDictionary 里昨忆,因?yàn)檫@樣會導(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];

上面這種書寫方式的效率和可讀性的都存在問題。

效率方面叔磷,一個(gè)不必要的不可變對象被創(chuàng)建后立馬被廢棄了拢驾;雖然這并不會讓你的 App 變慢(除非這個(gè)方法被頻繁調(diào)用),但是確實(shí)沒必要為了少打幾個(gè)字而這樣做改基。

可讀性方面繁疤,存在兩個(gè)問題:第一個(gè)問題是當(dāng)你瀏覽代碼并看見 @[] 的時(shí)候,你首先聯(lián)想到的是 NSArray 實(shí)例秕狰,但是在這種情形下你需要停下來深思熟慮的檢查稠腊;另一個(gè)問題是,一些新手以他的水平看到你的代碼后可能會對這是一個(gè)可變對象還是一個(gè)不可變對象產(chǎn)生分歧封恰。他/她可能不熟悉可變拷貝構(gòu)造的含義(這并不是說這個(gè)知識不重要)麻养。當(dāng)然褐啡,不存在絕對的錯(cuò)誤诺舔,我們只是討論代碼的可用性(包括可讀性)。

類名

類名應(yīng)該以個(gè)大寫字母作為前綴(雙字母前綴為 Apple 的類預(yù)留)备畦。盡管這個(gè)規(guī)范看起來有些古怪低飒,但是這樣做可以減少 Objective-C 沒有命名空間所帶來的問題。

一些開發(fā)者在定義模型對象時(shí)并不遵循這個(gè)規(guī)范(對于 Core Data 對象懂盐,我們更應(yīng)該遵循這個(gè)規(guī)范)褥赊。我們建議在定義 Core Data 對象時(shí)嚴(yán)格遵循這個(gè)約定,因?yàn)樽罱K你可能需要把你的 Managed Object Model(托管對象模型)與其他(第三方庫)的 MOMs(Managed Object Model)合并莉恼。

你可能注意到了拌喉,這本書里類的前綴(不僅僅是類,也包括公開的常量俐银、Protocol 等的前綴)是ZOC尿背。

另一個(gè)好的類的命名規(guī)范:當(dāng)你創(chuàng)建一個(gè)子類的時(shí)候,你應(yīng)該把說明性的部分放在前綴和父類名的在中間捶惜。

舉個(gè)例子:如果你有一個(gè) ZOCNetworkClient 類田藐,子類的名字會是ZOCTwitterNetworkClient (注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之間); 按照這個(gè)約定, 一個(gè)UIViewController 的子類會是 ZOCTimelineViewController.

Initializer 和 dealloc

推薦的代碼組織方式是將 dealloc 方法放在實(shí)現(xiàn)文件的最前面(直接在 @synthesize 以及 @dynamic 之后),init 應(yīng)該跟在 dealloc 方法后面汽久。

如果有多個(gè)初始化方法鹤竭, 指定初始化方法 (designated initializer) 應(yīng)該放在最前面,間接初始化方法 (secondary initializer) 跟在后面景醇,這樣更有邏輯性臀稚。如今有了 ARC,dealloc 方法幾乎不需要實(shí)現(xiàn)三痰,不過把 init 和 dealloc 放在一起可以從視覺上強(qiáng)調(diào)它們是一對的烁涌。通常,在 init 方法中做的事情需要在 dealloc 方法中撤銷酒觅。

init 方法應(yīng)該是這樣的結(jié)構(gòu):

- (instancetype)init
{
    self = [super init]; // call the designated initializer
    if (self) {
        // Custom initialization
    }
    return self;
}

為什么設(shè)置 self[super init] 的返回值撮执,以及中間發(fā)生了什么呢?這是一個(gè)十分有趣的話題舷丹。

我們退一步講:我們常常寫 [[NSObject alloc] init] 這樣的代碼抒钱,從而淡化了 allocinit 的區(qū)別。Objective-C 的這個(gè)特性叫做 兩步創(chuàng)建 颜凯。

這意味著申請分配內(nèi)存和初始化被分離成兩步谋币,allocinit

  • alloc 負(fù)責(zé)創(chuàng)建對象症概,這個(gè)過程包括分配足夠的內(nèi)存來保存對象蕾额,寫入 isa 指針,初始化引用計(jì)數(shù)彼城,以及重置所有實(shí)例變量诅蝶。
  • init 負(fù)責(zé)初始化對象,這意味著使對象處于可用狀態(tài)募壕。這通常意味著為對象的實(shí)例變量賦予合理有用的值调炬。

alloc 方法將返回一個(gè)有效的未初始化的對象實(shí)例。每一個(gè)對這個(gè)實(shí)例發(fā)送的消息會被轉(zhuǎn)換成一次 objc_msgSend() 函數(shù)的調(diào)用舱馅,形參 self 的實(shí)參是 alloc 返回的指針缰泡;這樣 self 在所有方法的作用域內(nèi)都能夠被訪問。

按照慣例代嗤,為了完成兩步創(chuàng)建棘钞,新創(chuàng)建的實(shí)例第一個(gè)被調(diào)用的方法將是 init 方法。注意干毅,NSObject 在實(shí)現(xiàn) init 時(shí)宜猜,只是簡單的返回了 self

關(guān)于 init 的約定還有一個(gè)重要部分:這個(gè)方法可以(并且應(yīng)該)通過返回 nil 來告訴調(diào)用者溶锭,初始化失敗了宝恶;初始化可能會因?yàn)楦鞣N原因失敗,比如一個(gè)輸入的格式錯(cuò)誤了,或者另一個(gè)需要的對象初始化失敗了垫毙。
這樣我們就能理解為什么總是需要調(diào)用 self = [super init]霹疫。如果你的父類說初始化自己的時(shí)候失敗了,那么你必須假定你正處于一個(gè)不穩(wěn)定的狀態(tài)综芥,因此在你的實(shí)現(xiàn)里不要繼續(xù)你自己的初始化并且也返回 nil丽蝎。如果不這樣做,你可能會操作一個(gè)不可用的對象膀藐,它的行為是不可預(yù)測的屠阻,最終可能會導(dǎo)致你的程序崩潰。

init 方法在被調(diào)用的時(shí)候可以通過重新給 self 重新賦值來返回另一個(gè)實(shí)例额各,而非調(diào)用的那個(gè)實(shí)例国觉。例如類簇,還有一些 Cocoa 類為相等的(不可變的)對象返回同一個(gè)實(shí)例虾啦。

Designated 和 Secondary 初始化方法

Objective-C 有指定初始化方法(designated initializer)和間接(secondary initializer)初始化方法的觀念麻诀。
designated 初始化方法是提供所有的參數(shù)讹堤,secondary 初始化方法是一個(gè)或多個(gè)洒宝,并且提供一個(gè)或者更多的默認(rèn)參數(shù)來調(diào)用 designated 初始化的初始化方法。

@implementation ZOCEvent

- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
                     location:(CLLocation *)location
{
    self = [super init];
    if (self) {
        _title    = title;
        _date     = date;
        _location = location;
    }
    return self;
}

- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
{
    return [self initWithTitle:title date:date location:nil];
}

- (instancetype)initWithTitle:(NSString *)title
{
    return [self initWithTitle:title date:[NSDate date] location:nil];
}

@end

initWithTitle:date:location: 就是 designated 初始化方法躏将,另外的兩個(gè)是 secondary 初始化方法硬毕。因?yàn)樗鼈儍H僅是調(diào)用類實(shí)現(xiàn)的 designated 初始化方法

Designated Initializer

一個(gè)類應(yīng)該有且只有一個(gè) designated 初始化方法呻引,其他的初始化方法應(yīng)該調(diào)用這個(gè) designated 的初始化方法(雖然這個(gè)情況有一個(gè)例外)

這個(gè)分歧沒有要求那個(gè)初始化函數(shù)需要被調(diào)用。

在類繼承中調(diào)用任何 designated 初始化方法都是合法的吐咳,而且應(yīng)該保證 所有的 designated initializer 在類繼承中是從祖先(通常是 NSObject)到你的類向下調(diào)用的逻悠。

實(shí)際上這意味著第一個(gè)執(zhí)行的初始化代碼是最遠(yuǎn)的祖先,然后從頂向下的類繼承挪丢,所有類都有機(jī)會執(zhí)行他們特定初始化代碼蹂风。這樣卢厂,你在做特定初始化工作前乾蓬,所有從超類繼承的東西都是不可用的狀態(tài)。 雖然這沒有明確的規(guī)定慎恒,但是所有 Apple 的框架都保證遵守這個(gè)約定任内,你的類也應(yīng)該這樣做。

當(dāng)定義一個(gè)新類的時(shí)候有三個(gè)不同的方式:

  1. 不需要重載任何初始化函數(shù)
  2. 重載 designated initializer
  3. 定義一個(gè)新的 designated initializer

第一個(gè)方案是最簡單的:你不需要增加類的任何初始化邏輯融柬,只需要依照父類的designated initializer死嗦。

當(dāng)你希望提供額外的初始化邏輯的時(shí)候,你可以重載 designated initializer粒氧。你只需要重載直接超類的 designated initializer 并且確認(rèn)你的實(shí)現(xiàn)調(diào)用了超類的方法越除。

一個(gè)典型的例子是你創(chuàng)造UIViewController子類的時(shí)候重載initWithNibName:bundle:方法。

@implementation ZOCViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call to the superclass designated initializer
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization (自定義的初始化過程)
    }
    return self;
}

@end

UIViewController 子類的例子里面如果重載 init 會是一個(gè)錯(cuò)誤,這個(gè)情況下調(diào)用者會嘗試調(diào)用 initWithNib:bundle 初始化你的類摘盆,你的類實(shí)現(xiàn)不會被調(diào)用翼雀。這同樣違背了它應(yīng)該是合法調(diào)用任何 designated initializer 的規(guī)則。

在你希望提供你自己的初始化函數(shù)的時(shí)候孩擂,你應(yīng)該遵守這三個(gè)步驟來保證獲得正確的行為:

  1. 定義你的 designated initializer狼渊,確保調(diào)用了直接超類的 designated initializer。
  2. 重載直接超類的 designated initializer类垦。調(diào)用你的新的 designated initializer狈邑。
  3. 為新的 designated initializer 寫文檔。

很多開發(fā)者忽略了后兩步蚤认,這不僅僅是一個(gè)粗心的問題米苹,而且這樣違反了框架的規(guī)則,可能導(dǎo)致不確定的行為和bug砰琢。
讓我們看看正確的實(shí)現(xiàn)的例子:

@implementation ZOCNewsViewController

- (id)initWithNews:(ZOCNews *)news
{
    // call to the immediate superclass's designated initializer (調(diào)用直接超類的 designated initializer)
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _news = news;
    }
    return self;
}

// Override the immediate superclass's designated initializer (重載直接父類的  designated initializer)
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call the new designated initializer
    return [self initWithNews:nil];
}

@end

如果你沒重載 initWithNibName:bundle: 驱入,而且調(diào)用者決定用這個(gè)方法初始化你的類(這是完全合法的)。 initWithNews: 永遠(yuǎn)不會被調(diào)用氯析,所以導(dǎo)致了不正確的初始化流程亏较,你的類的特定初始化邏輯沒有被執(zhí)行。

即使可以推斷那個(gè)方法是 designated initializer掩缓,也最好清晰地明確它(未來的你或者其他開發(fā)者在改代碼的時(shí)候會感謝你的)雪情。

你應(yīng)該考慮來用這兩個(gè)策略(不是互斥的):第一個(gè)是你在文檔中明確哪一個(gè)初始化方法是 designated 的,你可以用編譯器的指令 __attribute__((objc_designated_initializer)) 來標(biāo)記你的意圖你辣。

用這個(gè)編譯指令的時(shí)候巡通,編譯器會來幫你。如果你的新的 designated initializer 沒有調(diào)用超類的 designated initializer舍哄,那么編譯器會發(fā)出警告宴凉。

然而,當(dāng)沒有調(diào)用類的 designated initializer 的時(shí)候(并且依次提供必要的參數(shù))表悬,并且調(diào)用其他父類中的 designated initialize 的時(shí)候弥锄,會變成一個(gè)不可用的狀態(tài)。參考之前的例子蟆沫,當(dāng)實(shí)例化一個(gè) ZOCNewsViewController 展示一個(gè)新聞而那條新聞沒有展示的話籽暇,就會毫無意義。這個(gè)情況下你應(yīng)該只需要讓其他的 designated initializer 失效饭庞,來強(qiáng)制調(diào)用一個(gè)非常特別的 designated initializer戒悠。通過使用另外一個(gè)編譯器指令 __attribute__((unavailable("Invoke the designated initializer"))) 來修飾一個(gè)方法,通過這個(gè)屬性舟山,會讓你在試圖調(diào)用這個(gè)方法的時(shí)候產(chǎn)生一個(gè)編譯錯(cuò)誤绸狐。

這是之前的例子相關(guān)的實(shí)現(xiàn)的頭文件(這里使用宏來讓代碼沒有那么啰嗦)


@interface ZOCNewsViewController : UIViewController

- (instancetype)initWithNews:(ZOCNews *)news ZOC_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
- (instancetype)init ZOC_UNAVAILABLE_INSTEAD(initWithNews:);

@end

上述的一個(gè)推論是:你應(yīng)該永遠(yuǎn)不從 designated initializer 里面調(diào)用一個(gè) secondary initializer (如果secondary initializer 遵守約定卤恳,它會調(diào)用 designated initializer)。如果這樣寒矿,調(diào)用很可能會調(diào)用一個(gè)子類重寫的 init 方法并且陷入無限遞歸之中纬黎。

不過一個(gè)例外是一個(gè)對象是否遵守 NSCoding 協(xié)議,并且它通過方法 initWithCoder: 初始化劫窒。
我們應(yīng)該看超類是否符合 NSCoding 協(xié)議來區(qū)別對待本今。
符合的時(shí)候,如果你只是調(diào)用 [super initWithCoder:] 主巍,你可能需要在 designated initializer 里面寫一些通用的初始化代碼冠息,處理這種情況的一個(gè)好方法是把這些代碼放在私有方法里面(比如 p_commonInit )。
當(dāng)你的超類不符合 NSCoding 協(xié)議的時(shí)候孕索,推薦把 initWithCoder: 作為 secondary initializer 來對待逛艰,并且調(diào)用 self 的 designated initializer。 注意這違反了 Apple 寫在 Archives and Serializations Programming Guide 上面的規(guī)定:

the object should first invoke its superclass's designated initializer to initialize inherited state(對象總是應(yīng)該首先調(diào)用超類的 designated initializer 來初始化繼承的狀態(tài))

如果你的類不是 NSObject 的直接子類搞旭,這樣做的話散怖,會導(dǎo)致不可預(yù)測的行為。

Secondary Initializer

正如之前的描述肄渗,secondary initializer 是一種提供默認(rèn)值镇眷、行為到 designated initializer的方法。也就是說翎嫡,在這樣的方法里面你不應(yīng)該有初始化實(shí)例變量的操作欠动,并且你應(yīng)該一直假設(shè)這個(gè)方法不會得到調(diào)用。我們保證的是唯一被調(diào)用的方法是 designated initializer惑申。
這意味著你的 secondary initializer 總是應(yīng)該調(diào)用 Designated initializer 或者你自定義(上面的第三種情況:自定義Designated initializer)的 self的 designated initializer具伍。有時(shí)候,因?yàn)殄e(cuò)誤圈驼,可能打成了 super人芽,這樣會導(dǎo)致不符合上面提及的初始化順序(在這個(gè)特別的例子里面,是跳過當(dāng)前類的初始化)

參考

instancetype

我們經(jīng)常忽略 Cocoa 充滿了約定绩脆,并且這些約定可以幫助編譯器變得更加聰明萤厅。無論編譯器是否遭遇 alloc 或者 init 方法,他會知道衙伶,即使返回類型都是 id 祈坠,這些方法總是返回接受到的類類型的實(shí)例。因此矢劲,它允許編譯器進(jìn)行類型檢查。(比如慌随,檢查方法返回的類型是否合法)芬沉。Clang的這個(gè)好處來自于 related result type躺同, 意味著:

messages sent to one of alloc and init methods will have the same static type as the instance of the receiver class (發(fā)送到 alloc 或者 init 方法的消息會有同樣的靜態(tài)類型檢查是否為接受類的實(shí)例。)

更多的關(guān)于這個(gè)自動定義相關(guān)返回類型的約定請查看 Clang Language Extensions guide 的appropriate section

一個(gè)相關(guān)的返回類型可以明確地規(guī)定用 instancetype 關(guān)鍵字作為返回類型丸逸,并且它可以在一些工廠方法或者構(gòu)造器方法的場景下很有用蹋艺。它可以提示編譯器正確地檢查類型,并且更加重要的是黄刚,這同時(shí)適用于它的子類捎谨。

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name;
@end

雖然如此,根據(jù) clang 的定義憔维,id 可以被編譯器提升到 instancetype 涛救。在 alloc 或者 init 中,我們強(qiáng)烈建議對所有返回類的實(shí)例的類方法和實(shí)例方法使用 instancetype 類型业扒。

在你的 API 中要構(gòu)成習(xí)慣以及保持始終如一的检吆,此外,通過對你代碼的小調(diào)整你可以提高可讀性:在簡單的瀏覽的時(shí)候你可以區(qū)分哪些方法是返回你類的實(shí)例的程储。你以后會感謝這些注意過的小細(xì)節(jié)的蹭沛。

參考

初始化模式

類簇 (class cluster)

類簇在Apple的文檔中這樣描述:

an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個(gè)在共有的抽象超類下設(shè)置一組私有子類的架構(gòu))

如果這個(gè)描述聽起來很熟悉,說明你的直覺是對的章鲤。 Class cluster 是 Apple 對抽象工廠設(shè)計(jì)模式的稱呼摊灭。

class cluster 的想法很簡單: 使用信息進(jìn)行(類的)初始化處理期間,會使用一個(gè)抽象類(通常作為初始化方法的參數(shù)或者判定環(huán)境的可用性參數(shù))來完成特定的邏輯或者實(shí)例化一個(gè)具體的子類败徊。而這個(gè)"Public Facing(面向公眾的)"類斟或,必須非常清楚他的私有子類,以便在面對具體任務(wù)的時(shí)候有能力返回一個(gè)恰當(dāng)?shù)乃接凶宇悓?shí)例集嵌。對調(diào)用者來說只需知道對象的各種API的作用即可萝挤。這個(gè)模式隱藏了他背后復(fù)雜的初始化邏輯,調(diào)用者也不需要關(guān)心背后的實(shí)現(xiàn)根欧。

Class clusters 在 Apple 的Framework 中廣泛使用:一些明顯的例子比如 NSNumber 可以返回不同類型給你的子類怜珍,取決于 數(shù)字類型如何提供 (Integer, Float, etc...) 或者 NSArray 返回不同的最優(yōu)存儲策略的子類。

這個(gè)模式的精妙的地方在于凤粗,調(diào)用者可以完全不管子類酥泛,事實(shí)上,這可以用在設(shè)計(jì)一個(gè)庫嫌拣,可以用來交換實(shí)際的返回的類柔袁,而不用去管相關(guān)的細(xì)節(jié),因?yàn)樗鼈兌甲駨某橄蟪惖姆椒ā?/p>

我們的經(jīng)驗(yàn)是使用類簇可以幫助移除很多條件語句异逐。

一個(gè)經(jīng)典的例子是如果你有為 iPad 和 iPhone 寫的一樣的 UIViewController 子類捶索,但是在不同的設(shè)備上有不同的行為。

比較基礎(chǔ)的實(shí)現(xiàn)是用條件語句檢查設(shè)備灰瞻,然后執(zhí)行不同的邏輯腥例。雖然剛開始可能不錯(cuò)辅甥,但是隨著代碼的增長,運(yùn)行邏輯也會趨于復(fù)雜燎竖。
一個(gè)更好的實(shí)現(xiàn)的設(shè)計(jì)是創(chuàng)建一個(gè)抽象而且寬泛的 view controller 來包含所有的共享邏輯璃弄,并且對于不同設(shè)備有兩個(gè)特別的子例。

通用的 view controller 會檢查當(dāng)前設(shè)備并且返回適當(dāng)?shù)淖宇悺?/p>

@implementation ZOCKintsugiPhotoViewController

- (id)initWithPhotos:(NSArray *)photos
{
    if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
        self = nil;

        if ([UIDevice isPad]) {
            self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
        }
        else {
            self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
        }
        return self;
    }
    return [super initWithNibName:nil bundle:nil];
}

@end

這個(gè)子例程展示了如何創(chuàng)建一個(gè)類簇构回。

  1. 使用[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]防止子類中重載初始化方法夏块,避免無限遞歸。當(dāng)[[ZOCKintsugiPhotoViewController alloc] initWithPhotos:photos]被調(diào)用時(shí)纤掸,上面條件表達(dá)式的結(jié)果將會是True脐供。

  2. self = nil的目的是移除ZOCKintsugiPhotoViewController實(shí)例上的所有引用,實(shí)例(抽象類的實(shí)例)本身將會解除分配( 當(dāng)然ARC也好MRC也好dealloc都會發(fā)生在Main Runloop這一次的結(jié)束時(shí))茁肠。

  3. 接下來的邏輯就是判斷哪一個(gè)私有子類將被初始化患民。我們假設(shè)在iPhone上運(yùn)行這段代碼并且ZOCKintsugiPhotoViewController_iPhone沒有重載initWithPhotos:方法。這種情況下垦梆,當(dāng)執(zhí)行self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];,ZOCKintsugiPhotoViewController將會被調(diào)用匹颤,第一次檢查將會在這里發(fā)生,鑒于ZOCKintsugiPhotoViewController_iPhone不完全是ZOCKintsugiPhotoViewController托猩,表達(dá)式[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]將會是False,于是就會調(diào)用[super initWithNibName:nil bundle:nil]印蓖,于是就會進(jìn)入ZOCKintsugiPhotoViewController的初始化過程,這時(shí)候因?yàn)檎{(diào)用者就是ZOCKintsugiPhotoViewController本身京腥,這一次的檢查必定為True,接下來就會進(jìn)行正確的初始化過程赦肃。(NOTE:這里必須是完全遵循Designated initializer 以及Secondary initializer的設(shè)計(jì)規(guī)范的前提下才會其效果的!不明白這個(gè)規(guī)范的可以后退一步熟悉這種規(guī)范在回頭來看這個(gè)說明)

NOTE: 這里的意思是,代碼是在iPhone上調(diào)試的公浪,程序員使用了self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];來初始化某個(gè)view controller的對象他宛,當(dāng)代碼運(yùn)行在iPad上時(shí),這個(gè)初始化過程也是正確的欠气,因?yàn)闊o論程序員的代碼中使用self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];來初始化viewController(iPhone上編寫運(yùn)行在iPad上)厅各,還是使用self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];來初始化viewController(iPad上編寫,運(yùn)行在iPhone上)预柒,都會因?yàn)閆OCKintsugiPhotoViewController的initWithPhotos:方法的存在而變得通用起來队塘。

單例

如果可能,請盡量避免使用單例而是依賴注入宜鸯。
然而憔古,如果一定要用,請使用一個(gè)線程安全的模式來創(chuàng)建共享的實(shí)例淋袖。對于 GCD鸿市,用 dispatch_once() 函數(shù)就可以咯。

+ (instancetype)sharedInstance
{
   static id sharedInstance = nil;
   static dispatch_once_t onceToken = 0;
   dispatch_once(&onceToken, ^{
      sharedInstance = [[self alloc] init];
   });
   return sharedInstance;
}

使用 dispatch_once()适贸,來控制代碼同步灸芳,取代了原來的約定俗成的用法涝桅。

+ (instancetype)sharedInstance
{
    static id sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[MyClass alloc] init];
        }
    }
    return sharedInstance;
}

dispatch_once() 的優(yōu)點(diǎn)是拜姿,它更快烙样,而且語法上更干凈,因?yàn)閐ispatch_once()的意思就是 “把一些東西執(zhí)行一次”蕊肥,就像我們做的一樣谒获。 這樣同時(shí)可以避免 possible and sometimes prolific crashes.

經(jīng)典的單例對象是:一個(gè)設(shè)備的GPS以及它的加速度傳感器(也稱動作感應(yīng)器)。
雖然單例對象可以子類化壁却,但這種方式能夠有用的情況非常少見批狱。
必須有證據(jù)表明,給定類的接口趨向于作為單例來使用展东。
所以赔硫,單例通常公開一個(gè)sharedInstance的類方法就已經(jīng)足夠了,沒有任何的可寫屬性需要被暴露出來盐肃。

嘗試著把單例作為一個(gè)對象的容器爪膊,在代碼或者應(yīng)用層面上共享,是一個(gè)糟糕和丑陋的設(shè)計(jì)砸王。

NOTE:單例模式應(yīng)該運(yùn)用于類及類的接口趨向于作為單例來使用的情況 (譯者注)

屬性

屬性應(yīng)該盡可能描述性地命名推盛,避免縮寫,并且是小寫字母開頭的駝峰命名谦铃。我們的工具可以很方便地幫我們自動補(bǔ)全所有東西(嗯耘成。。幾乎所有的驹闰,Xcode 的Derived Data 會索引這些命名)瘪菌。所以沒理由少打幾個(gè)字符了,并且最好盡可能在你源碼里表達(dá)更多東西嘹朗。

例子 :

NSString *text;

不要這樣 :

NSString* text;
NSString * text;

(注意:這個(gè)習(xí)慣和常量不同师妙,這是主要從常用和可讀性考慮。 C++ 的開發(fā)者偏好從變量名中分離類型骡显,作為類型它應(yīng)該是
NSString* (對于從堆中分配的對象疆栏,對于C++是能從棧上分配的)格式。)

使用屬性的自動同步 (synthesize) 而不是手動的 @synthesize 語句惫谤,除非你的屬性是 protocol 的一部分而不是一個(gè)完整的類壁顶。如果 Xcode 可以自動同步這些變量,就讓它來做吧溜歪。否則只會讓你拋開 Xcode 的優(yōu)點(diǎn)若专,維護(hù)更冗長的代碼。

你應(yīng)該總是使用 setter 和 getter 方法訪問屬性蝴猪,除了 initdealloc 方法。通常,使用屬性讓你增加了在當(dāng)前作用域之外的代碼塊的可能所以可能帶來更多副作用吮便。

你總應(yīng)該用 getter 和 setter 铃将,因?yàn)椋?/p>

  • 使用 setter 會遵守定義的內(nèi)存管理語義(strong, weak, copy etc...) ,這個(gè)在 ARC 之前就是相關(guān)的內(nèi)容骑丸。舉個(gè)例子,copy 屬性定義了每個(gè)時(shí)候你用 setter 并且傳送數(shù)據(jù)的時(shí)候,它會復(fù)制數(shù)據(jù)而不用額外的操作赃额。
  • KVO 通知(willChangeValueForKey, didChangeValueForKey) 會被自動執(zhí)行。
  • 更容易debug:你可以設(shè)置一個(gè)斷點(diǎn)在屬性聲明上并且斷點(diǎn)會在每次 getter / setter 方法調(diào)用的時(shí)候執(zhí)行叫确,或者你可以在自己的自定義 setter/getter 設(shè)置斷點(diǎn)跳芳。
  • 允許在一個(gè)單獨(dú)的地方為設(shè)置值添加額外的邏輯。

你應(yīng)該傾向于用 getter:

  • 它是對未來的變化有擴(kuò)展能力的(比如竹勉,屬性是自動生成的)飞盆。
  • 它允許子類化。
  • 更簡單的debug(比如次乓,允許拿出一個(gè)斷點(diǎn)在 getter 方法里面吓歇,并且看誰訪問了特別的 getter
  • 它讓意圖更加清晰和明確:通過訪問 ivar _anIvar 你可以明確的訪問 self->_anIvar.這可能導(dǎo)致問題。在 block 里面訪問 ivar (你捕捉并且 retain 了 self檬输,即使你沒有明確的看到 self 關(guān)鍵詞)照瘾。
  • 它自動產(chǎn)生KVO 通知。
  • 在消息發(fā)送的時(shí)候增加的開銷是微不足道的丧慈。更多關(guān)于性能問題的介紹你可以看 Should I Use a Property or an Instance Variable?析命。

Init 和 Dealloc

有一個(gè)例外:永遠(yuǎn)不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你應(yīng)當(dāng)直接訪問實(shí)例變量逃默。這樣做是為了防止有子類時(shí)鹃愤,出現(xiàn)這樣的情況:它的子類最終重載了其 setter 或者 getter 方法,因此導(dǎo)致該子類去調(diào)用其他的方法完域、訪問那些處于不穩(wěn)定狀態(tài)软吐,或者稱為沒有初始化完成的屬性或者 ivar 。記住一個(gè)對象僅僅在 init 返回的時(shí)候吟税,才會被認(rèn)為是達(dá)到了初始化完成的狀態(tài)凹耙。

同樣在 dealloc 方法中(在 dealloc 方法中,一個(gè)對象可以在一個(gè) 不確定的狀態(tài)中)這是同樣需要被注意的肠仪。

此外肖抱,在 init 中使用 setter 不會很好執(zhí)行 UIAppearence 代理(參見 UIAppearance for Custom Views 看更多相關(guān)信息)。

點(diǎn)符號

當(dāng)使用 setter getter 方法的時(shí)候盡量使用點(diǎn)符號异旧。應(yīng)該總是用點(diǎn)符號來訪問以及設(shè)置屬性意述。

例子:

view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;

不要這樣:

[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;

使用點(diǎn)符號會讓表達(dá)更加清晰并且?guī)椭鷧^(qū)分屬性訪問和方法調(diào)用

屬性定義

推薦按照下面的格式來定義屬性

@property (nonatomic, readwrite, copy) NSString *name;

屬性的參數(shù)應(yīng)該按照下面的順序排列: 原子性,讀寫 和 內(nèi)存管理。 這樣做你的屬性更容易修改正確荤崇,并且更好閱讀拌屏。(譯者注:習(xí)慣上修改某個(gè)屬性的修飾符時(shí),一般從屬性名從右向左搜索需要修動的修飾符术荤。最可能從最右邊開始修改這些屬性的修飾符倚喂,根據(jù)經(jīng)驗(yàn)這些修飾符被修改的可能性從高到底應(yīng)為:內(nèi)存管理 > 讀寫權(quán)限 >原子操作)

你必須使用 nonatomic,除非特別需要的情況喜每。在iOS中务唐,atomic帶來的鎖特別影響性能雳攘。

屬性可以存儲一個(gè)代碼塊带兜。為了讓它存活到定義的塊的結(jié)束,必須使用 copy (block 最早在棧里面創(chuàng)建吨灭,使用 copy讓 block 拷貝到堆里面去)

為了完成一個(gè)共有的 getter 和一個(gè)私有的 setter刚照,你應(yīng)該聲明公開的屬性為 readonly 并且在類擴(kuò)展中重新定義通用的屬性為 readwrite 的。

//.h文件中
@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object;
@end
//.m文件中
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object;
@end

@implementation MyClass
//Do Something cool
@end

描述BOOL屬性的詞如果是形容詞喧兄,那么setter不應(yīng)該帶is前綴无畔,但它對應(yīng)的 getter 訪問器應(yīng)該帶上這個(gè)前綴,如:

@property (assign, getter=isEditable) BOOL editable;

文字和例子引用自 Cocoa Naming Guidelines吠冤。

在實(shí)現(xiàn)文件中應(yīng)避免使用@synthesize,因?yàn)閄code已經(jīng)自動為你添加了浑彰。

私有屬性

私有屬性應(yīng)該定義在類的實(shí)現(xiàn)文件的類的擴(kuò)展 (匿名的 category) 中。不允許在有名字的 category(如 ZOCPrivate)中定義私有屬性拯辙,除非你擴(kuò)展其他類郭变。

例子:

@interface ZOCViewController ()
@property (nonatomic, strong) UIView *bannerView;
@end

可變對象

任何可以用一個(gè)可變的對象設(shè)置的((比如 NSString,NSArray,NSURLRequest))屬性的內(nèi)存管理類型必須是 copy 的。

這是為了確保防止在不明確的情況下修改被封裝好的對象的值(譯者注:比如執(zhí)行 array(定義為 copy 的 NSArray 實(shí)例) = mutableArray涯保,copy 屬性會讓 array 的 setter 方法為 array = [mutableArray copy], [mutableArray copy] 返回的是不可變的 NSArray 實(shí)例诉濒,就保證了正確性。用其他屬性修飾符修飾夕春,容易在直接賦值的時(shí)候未荒,array 指向的是 NSMuatbleArray 的實(shí)例,在之后可以隨意改變它的值及志,就容易出錯(cuò))片排。

你應(yīng)該同時(shí)避免暴露在公開的接口中可變的對象,因?yàn)檫@允許你的類的使用者改變類自己的內(nèi)部表示并且破壞類的封裝速侈。你可以提供可以只讀的屬性來返回你對象的不可變的副本率寡。

/* .h */
@property (nonatomic, readonly) NSArray *elements

/* .m */
- (NSArray *)elements {
  return [self.mutableElements copy];
}

懶加載(Lazy Loading)

當(dāng)實(shí)例化一個(gè)對象需要耗費(fèi)很多資源,或者配置一次就要調(diào)用很多配置相關(guān)的方法而你又不想弄亂這些方法時(shí)锌畸,我們需要重寫 getter 方法以延遲實(shí)例化勇劣,而不是在 init 方法里給對象分配內(nèi)存。通常這種操作使用下面這樣的模板:


- (NSDateFormatter *)dateFormatter {
  if (!_dateFormatter) {
    _dateFormatter = [[NSDateFormatter alloc] init];
        NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        [_dateFormatter setLocale:enUSPOSIXLocale];
        [_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];//毫秒是SSS,而非SSSSS
  }
  return _dateFormatter;
}

即使這樣做在某些情況下很不錯(cuò)比默,但是在實(shí)際這樣做之前應(yīng)當(dāng)深思熟慮幻捏。事實(shí)上,這樣的做法是可以避免的命咐。下面是使用延遲實(shí)例化的爭議篡九。

  • getter 方法應(yīng)該避免副作用〈椎欤看到 getter 方法的時(shí)候榛臼,你不會想到會因此創(chuàng)建一個(gè)對象或?qū)е赂弊饔茫瑢?shí)際上如果調(diào)用 getter 方法而不使用其返回值編譯器會報(bào)警告 “Getter 不應(yīng)該僅因它產(chǎn)生的副作用而被調(diào)用”窜司。

副作用指當(dāng)調(diào)用函數(shù)時(shí)沛善,除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響塞祈。例如修改全局變量(函數(shù)外的變量)或修改參數(shù)金刁。函數(shù)副作用會給程序設(shè)計(jì)帶來不必要的麻煩,給程序帶來十分難以查找的錯(cuò)誤议薪,并且降低程序的可讀性尤蛮。(譯者注)

  • 你在第一次訪問的時(shí)候改變了初始化的消耗,產(chǎn)生了副作用斯议,這會讓優(yōu)化性能變得困難(以及測試)
  • 這個(gè)初始化可能是不確定的:比如你期望屬性第一次被一個(gè)方法訪問产捞,但是你改變了類的實(shí)現(xiàn),訪問器在你預(yù)期之前就得到了調(diào)用哼御,這樣可以導(dǎo)致問題坯临,特別是初始化邏輯可能依賴于類的其他不同狀態(tài)的時(shí)候⊥Р螅總的來說最好明確依賴關(guān)系尿扯。
  • 這個(gè)行為不是 KVO 友好的。如果 getter 改變了引用焰雕,他應(yīng)該通過一個(gè) KVO 通知來通知改變衷笋。當(dāng)訪問 getter 的時(shí)候收到一個(gè)改變的通知很奇怪。

方法

參數(shù)斷言

你的方法可能要求一些參數(shù)來滿足特定的條件(比如不能為nil)矩屁,在這種情況下最好使用 NSParameterAssert() 來斷言條件是否成立或是拋出一個(gè)異常辟宗。

私有方法

永遠(yuǎn)不要在你的私有方法前加上 _ 前綴。這個(gè)前綴是 Apple 保留的吝秕。不要冒重載蘋果的私有方法的險(xiǎn)泊脐。

相等性

當(dāng)你要實(shí)現(xiàn)相等性的時(shí)候記住這個(gè)約定:你需要同時(shí)實(shí)現(xiàn)isEqualhash方法。如果兩個(gè)對象是被isEqual認(rèn)為相等的烁峭,它們的 hash 方法需要返回一樣的值容客。但是如果 hash 返回一樣的值秕铛,并不能確保他們相等。

這個(gè)約定當(dāng)對象被存儲在集合中(如 NSDictionaryNSSet 在底層使用 hash 表數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu))的時(shí)候缩挑,用來查找這些對象的但两。

@implementation ZOCPerson

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[ZOCPerson class]]) {
        return NO;
    }

    // check objects properties (name and birthday) for equality (檢查對象屬性(名字和生日)的相等性
    ...
    return propertiesMatch;
}

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}

@end

一定要注意 hash 方法不能返回一個(gè)常量。這是一個(gè)典型的錯(cuò)誤并且會導(dǎo)致嚴(yán)重的問題供置,因?yàn)閷?shí)際上hash方法的返回值會作為對象在 hash 散列表中的 key,這會導(dǎo)致 hash 表 100% 的碰撞谨湘。

你總是應(yīng)該用 isEqualTo<#class-name-without-prefix#>: 這樣的格式實(shí)現(xiàn)一個(gè)相等性檢查方法。如果你這樣做芥丧,會優(yōu)先調(diào)用這個(gè)方法來避免上面的類型檢查紧阔。

一個(gè)完整的 isEqual 方法應(yīng)該是這樣的:



- (BOOL)isEqual:(id)object {
    if (self == object) {
      return YES;
    }

    if (![object isKindOfClass:[ZOCPerson class]]) {
      return NO;
    }

    return [self isEqualToPerson:(ZOCPerson *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
    if (!person) {
        return NO;
    }

    BOOL namesMatch = (!self.name && !person.name) ||
                       [self.name isEqualToString:person.name];
    BOOL birthdaysMatch = (!self.birthday && !person.birthday) ||
                           [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

譯者注:
一般而言我們會直接調(diào)用自定義的isEqualTo__ClassName__:方法,對類的實(shí)例判等续担。

像相等性的開篇已經(jīng)提到的那樣擅耽,這里應(yīng)該復(fù)寫isEqual:方法,因?yàn)镹SObject的isEqual:方法顯然不會考慮我們自定義類的類型判斷及屬性的相等性赤拒。當(dāng)我們自定義的類的對象處在無序集合中被查找時(shí)秫筏,會自動調(diào)用isEqual:。同樣的該類的hash方法挎挖,也會在集合查找對象的時(shí)候被使用,我們也可以通過復(fù)寫hash方法以達(dá)到用自己的標(biāo)準(zhǔn)來判定對象是否hash等同航夺。

我們實(shí)現(xiàn)的hash方法應(yīng)該建立在系統(tǒng)提供的各種對象的hash方法之上(像開篇的例程那樣)蕉朵。不推薦自己去實(shí)現(xiàn)某種hash算法來替代系統(tǒng)提供的hash算法,這一般而言會大大影響性能或者準(zhǔn)確性阳掐,系統(tǒng)提供的hash算法已經(jīng)經(jīng)過無數(shù)次修繕始衅,足以滿足你的要求。

一個(gè)對象實(shí)例的 hash 計(jì)算結(jié)果應(yīng)該是確定的缭保。當(dāng)它被加入到一個(gè)容器對象(比如 NSArray, NSSet, 或者 NSDictionary)的時(shí)候這是很重要的汛闸,否則行為會無法預(yù)測(所有的容器對象使用對象的 hash 來查找或者實(shí)施特別的行為,如確定唯一性)這也就是說艺骂,應(yīng)該用不可變的屬性來計(jì)算 hash 值诸老,或者,最好保證對象是不可變的钳恕。

Categories

雖然我們知道這樣寫很丑, 但是我們應(yīng)該要在我們的 category 方法前加上自己的小寫前綴以及下劃線别伏,比如- (id)zoc_myCategoryMethod。 這種實(shí)踐同樣被蘋果推薦忧额。

這是非常必要的厘肮。因?yàn)槿绻跀U(kuò)展的 category 或者其他 category 里面已經(jīng)使用了同樣的方法名,會導(dǎo)致不可預(yù)計(jì)的后果睦番。實(shí)際上类茂,實(shí)際被調(diào)用的是最后被加載的那個(gè) category 中方法的實(shí)現(xiàn)(譯者注:如果導(dǎo)入的多個(gè) category 中有一些同名的方法導(dǎo)入到類里時(shí)耍属,最終調(diào)用哪個(gè)是由編譯時(shí)的加載順序來決定的,最后一個(gè)加載進(jìn)來的方法會覆蓋之前的方法)巩检。

如果想要確認(rèn)你的分類方法沒有覆蓋其他實(shí)現(xiàn)的話恬涧,可以把環(huán)境變量 OBJC_PRINT_REPLACED_METHODS 設(shè)置為 YES,這樣那些被取代的方法名字會打印到 Console 中〔杲恚現(xiàn)在 LLVM 5.1 不會為此發(fā)出任何警告和錯(cuò)誤提示溯捆,所以自己小心不要在分類中重載方法。

一個(gè)好的實(shí)踐是在 category 名中使用前綴厦瓢。

** 例子 **

@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end

** 不要這樣 **

@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end

分類可以用來在頭文件中定義一組功能相似的方法提揍。這是在 Apple的 Framework 也很常見的一個(gè)實(shí)踐(下面例子的取自NSDate 頭文件)。我們也強(qiáng)烈建議在自己的代碼中這樣使用煮仇。

我們的經(jīng)驗(yàn)是劳跃,創(chuàng)建一組分類對以后的重構(gòu)十分有幫助。一個(gè)類的接口增加的時(shí)候浙垫,可能意味著你的類做了太多事情刨仑,違背了類的單一功能原則。

之前創(chuàng)造的方法分組可以用來更好地進(jìn)行不同功能的表示夹姥,并且把類打破在更多自我包含的組成部分里杉武。


@interface NSDate : NSObject <NSCopying, NSSecureCoding>

@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;

@end

@interface NSDate (NSDateCreation)

+ (instancetype)date;
+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
+ (instancetype)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
// ...
@end

Protocols

在 Objective-C 的世界里面經(jīng)常錯(cuò)過的一個(gè)東西是抽象接口。接口(interface)這個(gè)詞通常指一個(gè)類的 .h 文件辙售,但是它在 Java 程序員眼里有另外的含義: 一系列不依賴具體實(shí)現(xiàn)的方法的定義轻抱。(譯者注:在OC中,類的接口對應(yīng)在.m文件中都會有具體的實(shí)現(xiàn)旦部,但Java中接口更接近于OC中的抽象接口或者說協(xié)議(protocol))

在 Objective-C 里是通過 protocol 來實(shí)現(xiàn)抽象接口的祈搜。因?yàn)闅v史原因,protocol (使用方法類似java的接口)并沒有大量地在Objective-C的代碼中使用也沒有在社區(qū)中普及(指的是那種像Java程序員使用接口那樣來使用protocol的方式)士八。一個(gè)主要原因是大多數(shù)的 Apple 開發(fā)的代碼沒有采用這種的方式容燕,而幾乎所有的開發(fā)者都是遵從 Apple 的模式以及指南。Apple 幾乎只是在委托模式下使用 protocol婚度。

但是抽象接口的概念很強(qiáng)大蘸秘,在計(jì)算機(jī)科學(xué)的歷史中頗有淵源,沒有理由不在 Objective-C 中使用陕见。

這里通過一個(gè)具體的例子來解釋 protocol 的強(qiáng)大力量(用作抽象接口):把非常糟糕的設(shè)計(jì)的架構(gòu)改造為一個(gè)良好的可復(fù)用的代碼秘血。

這個(gè)例子是在實(shí)現(xiàn)一個(gè) RSS 閱讀器(它可是經(jīng)常在技術(shù)面試中作為一個(gè)測試題呢)。

要求很簡單:在TableView中展示一個(gè)遠(yuǎn)程的RSS訂閱评甜。

一個(gè)幼稚的方法是創(chuàng)建一個(gè) UITableViewController 的子類灰粮,并且把所有的檢索訂閱數(shù)據(jù),解析以及展示的邏輯放在一起忍坷,或者說是一個(gè) MVC (Massive View Controller)粘舟。這可以跑起來熔脂,但是它的設(shè)計(jì)非常糟糕,不過它足夠過一些要求不高的面試了柑肴。

最小的步驟是遵從單一功能原則霞揉,創(chuàng)建至少兩個(gè)組成部分來完成這個(gè)任務(wù):

  • 一個(gè) feed 解析器來解析搜集到的結(jié)果
  • 一個(gè) feed 閱讀器來顯示結(jié)果

這些類的接口可以是這樣的:


@interface ZOCFeedParser : NSObject

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;

- (id)initWithURL:(NSURL *)url;

- (BOOL)start;
- (void)stop;

@end


@interface ZOCTableViewController : UITableViewController

- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser;

@end

ZOCFeedParserNSURL 進(jìn)行初始化,來獲取 RSS 訂閱(在這之下可能會使用 NSXMLParser 和 NSXMLParserDelegate 創(chuàng)建有意義的數(shù)據(jù))晰骑,ZOCTableViewController 會用這個(gè) parser 來進(jìn)行初始化适秩。 我們希望它顯示 parser 接受到的值并且我們用下面的 protocol 實(shí)現(xiàn)委托:


@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;
@end

我要說,這是一個(gè)處理RSS業(yè)務(wù)的完全合理而恰當(dāng)?shù)膒rotocol硕舆。這個(gè)ViewController在Public接口中將遵循這個(gè)protocol:

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>

最后創(chuàng)建的代碼是這樣子的:

NSURL *feedURL = [NSURL URLWithString:@"http://www.bbc.co.uk/feed.rss"];

ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL];

ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;

到目前你可能覺得你的代碼還是不錯(cuò)的秽荞,但是有多少代碼是可以有效復(fù)用的呢?view controller 只能處理 ZOCFeedParser 類型的對象: 從這點(diǎn)來看我們只是把代碼分離成了兩個(gè)組成部分抚官,而沒有做任何其他有價(jià)值的事情扬跋。

view controller 的職責(zé)應(yīng)該是“顯示某些東西提供的內(nèi)容”,但是如果我們只允許傳遞ZOCFeedParser的話凌节,就不是這樣的了钦听。這就體現(xiàn)了需要傳遞給 view controller 一個(gè)更泛型的對象的需求。

我們使用 ZOCFeedParserProtocol 這個(gè) protocol (在 ZOCFeedParserProtocol.h 文件里面倍奢,同時(shí)文件里也有 ZOCFeedParserDelegate )朴上。


@protocol ZOCFeedParserProtocol <NSObject>

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;

- (BOOL)start;
- (void)stop;

@end

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;
@end

注意這個(gè)代理 protocol 現(xiàn)在處理響應(yīng)我們新的 protocol, 而且 ZOCFeedParser 的接口文件更加精煉了:


@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol>

- (id)initWithURL:(NSURL *)url;

@end

因?yàn)?ZOCFeedParser 實(shí)現(xiàn)了 ZOCFeedParserProtocol娱挨,它需要實(shí)現(xiàn)所有的required方法余指。
從這點(diǎn)來看 viewController能接受任何遵循該協(xié)議的對象,只要確保所有的對象都會響應(yīng)startstop方法并通過delegate屬性提供信息(譯者注:因?yàn)閜rotocol默認(rèn)情況下所有的方法定義都是required的)跷坝。對指定的對象而言,這就是viewController所要知道的一切,且不需要知道其實(shí)現(xiàn)的細(xì)節(jié)碉碉。


@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>

- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;

@end

上面的代碼片段的改變看起來不多柴钻,但是有了一個(gè)巨大的提升。view controller 將基于協(xié)議而不是具體的實(shí)現(xiàn)來工作垢粮。這帶來了以下的優(yōu)點(diǎn):

  • view controller 現(xiàn)在可以接收通過delegate屬性提供信息的任意對象:可以是 RSS 遠(yuǎn)程解析器贴届,或者本地解析器,或是一個(gè)讀取其他遠(yuǎn)程或者本地?cái)?shù)據(jù)的服務(wù)
  • ZOCFeedParserZOCFeedParserDelegate 可以被其他組成部分復(fù)用
  • ZOCViewController (UI邏輯部分)可以被復(fù)用
  • 測試更簡單了蜡吧,因?yàn)榭梢杂?mock 對象來達(dá)到 protocol 預(yù)期的效果

當(dāng)實(shí)現(xiàn)一個(gè) protocol 你總應(yīng)該堅(jiān)持 里氏替換原則毫蚓。這個(gè)原則是:你應(yīng)該可以取代任意接口(也就是Objective-C里的"protocol")實(shí)現(xiàn),而不用改變客戶端或者相關(guān)實(shí)現(xiàn)昔善。

此外元潘,這也意味著protocol不該關(guān)心類的實(shí)現(xiàn)細(xì)節(jié);設(shè)計(jì)protocol的抽象表述時(shí)應(yīng)非常用心君仆,并且要牢記它和它背后的實(shí)現(xiàn)是不相干的翩概,真正重要的是協(xié)議(這個(gè)暴露給使用者的抽象表述)牲距。

任何在未來可復(fù)用的設(shè)計(jì),無形當(dāng)中可以提高代碼質(zhì)量钥庇,這也應(yīng)該一直是程序員的追求。是否這樣設(shè)計(jì)代碼,就是大師和菜鳥的區(qū)別纺且。

最后的代碼可以在這里 找到踩身。

NSNotification

當(dāng)你定義你自己的 NSNotification 的時(shí)候你應(yīng)該把你的通知的名字定義為一個(gè)字符串常量,就像你暴露給其他類的其他字符串常量一樣吐句。你應(yīng)該在公開的接口文件中將其聲明為 extern 的胁后, 并且在對應(yīng)的實(shí)現(xiàn)文件里面定義。

因?yàn)槟阍陬^文件中暴露了符號蕴侧,所以你應(yīng)該按照統(tǒng)一的命名空間前綴法則择同,用類名前綴作為這個(gè)通知名字的前綴。

同時(shí)净宵,用一個(gè) Did/Will 這樣的動詞以及用 "Notifications" 后綴來命名這個(gè)通知也是一個(gè)好的實(shí)踐敲才。

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

美化代碼

空格

  • 縮進(jìn)使用 4 個(gè)空格。 永遠(yuǎn)不要使用 tab, 確保你在 Xcode 的設(shè)置里面是這樣設(shè)置的择葡。
  • 方法的大括號和其他的大括號(if/else/switch/while 等) 總是在同一行開始紧武,在新起一行結(jié)束。

推薦:

if (user.isHappy) {
    //Do something
}
else {
    //Do something else
}

不推薦:

if (user.isHappy)
{
  //Do something
} else {
  //Do something else
}
  • 方法之間應(yīng)該要有一個(gè)空行來幫助代碼看起來清晰且有組織敏储。 方法內(nèi)的空格應(yīng)該用來分離功能阻星,但是通常不同的功能應(yīng)該用新的方法來定義。
  • 優(yōu)先使用 auto-synthesis已添。但是如果必要的話妥箕, @synthesize and @dynamic
  • 在實(shí)現(xiàn)文件中的聲明應(yīng)該新起一行。
  • 應(yīng)該總是讓冒號對齊更舞。有一些方法簽名可能超過三個(gè)冒號畦幢,用冒號對齊可以讓代碼更具有可讀性。即使有代碼塊存在缆蝉,也應(yīng)該用冒號對齊方法宇葱。

推薦:

[UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];

不推薦:

[UIView animateWithDuration:1.0 animations:^{
    // something
} completion:^(BOOL finished) {
    // something
}];

如果自動對齊讓可讀性變得糟糕,那么應(yīng)該在之前把 block 定義為變量刊头,或者重新考慮你的代碼簽名設(shè)計(jì)黍瞧。

換行

本指南關(guān)注代碼顯示效果以及在線瀏覽的可讀性,所以換行是一個(gè)重要的主題原杂。

舉個(gè)例子:

self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];

一個(gè)像上面的長行的代碼在第二行以一個(gè)間隔(2個(gè)空格)延續(xù)

self.productsRequest = [[SKProductsRequest alloc]
  initWithProductIdentifiers:productIdentifiers];

括號

在以下的地方使用 Egyptian風(fēng)格 括號 (譯者注:又稱 K&R 風(fēng)格印颤,代碼段括號的開始位于一行的末尾,而不是另外起一行的風(fēng)格污尉。關(guān)于為什么叫做 Egyptian Brackets膀哲,可以參考 http://blog.codinghorror.com/new-programming-jargon/ )

  • 控制語句 (if-else, for, switch)

非 Egyptian 括號可以用在:

  • 類的實(shí)現(xiàn)(如果存在)
  • 方法的實(shí)現(xiàn)

代碼組織

來自 Mattt Thompson

code organization is a matter of hygiene (代碼組織是衛(wèi)生問題)

我們十分贊成這句話往产。清晰地組織代碼和規(guī)范地進(jìn)行定義, 是你對自己以及其他閱讀代碼的人的尊重。

利用代碼塊

一個(gè) GCC 非常模糊的特性某宪,以及 Clang 也有的特性是仿村,代碼塊如果在閉合的圓括號內(nèi)的話,會返回最后語句的值


NSURL *url = ({
    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
    [NSURL URLWithString:urlString];
});

Pragma

Pragma Mark

#pragma mark - 是一個(gè)在類內(nèi)部組織代碼并且?guī)椭惴纸M方法實(shí)現(xiàn)的好辦法兴喂。 我們建議使用 #pragma mark - 來分離:

  • 不同功能組的方法
  • protocols 的實(shí)現(xiàn)
  • 對父類方法的重寫

- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

#pragma mark - View Lifecycle (View 的生命周期)

- (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)zoc_privateMethod { /* ... */ }

#pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }

#pragma mark - ZOCSuperclass

// ... 重載來自 ZOCSuperclass 的方法

#pragma mark - NSObject

- (NSString *)description { /* ... */ }

上面的標(biāo)記能明顯分離和組織代碼蔼囊。你還可以用 cmd+Click 來快速跳轉(zhuǎn)到符號定義地方。
但是小心衣迷,即使 paragma mark 是一門手藝畏鼓,但是它不是讓你類里面方法數(shù)量增加的一個(gè)理由:類里面有太多方法說明類做了太多事情,需要考慮重構(gòu)了壶谒。

關(guān)于 pragma

http://raptureinvenice.com/pragmas-arent-just-for-marks/ 有很好的關(guān)于 pragma 的討論了云矫,在這邊我們再做部分說明。

大多數(shù) iOS 開發(fā)者平時(shí)并沒有和很多編譯器選項(xiàng)打交道汗菜。一些選項(xiàng)是對控制嚴(yán)格檢查(或者不檢查)你的代碼或者錯(cuò)誤的让禀。有時(shí)候,你想要用 pragma 直接產(chǎn)生一個(gè)異常陨界,臨時(shí)打斷編譯器的行為巡揍。

當(dāng)你使用ARC的時(shí)候,編譯器幫你插入了內(nèi)存管理相關(guān)的調(diào)用菌瘪。但是這樣可能產(chǎn)生一些煩人的事情腮敌。比如你使用 NSSelectorFromString 來動態(tài)地產(chǎn)生一個(gè) selector 調(diào)用的時(shí)候,ARC不知道這個(gè)方法是哪個(gè)并且不知道應(yīng)該用那種內(nèi)存管理方法俏扩,你會被提示 performSelector may cause a leak because its selector is unknown(執(zhí)行 selector 可能導(dǎo)致泄漏糜工,因?yàn)檫@個(gè) selector 是未知的).

如果你知道你的代碼不會導(dǎo)致內(nèi)存泄露,你可以通過加入這些代碼忽略這些警告

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[myObj performSelector:mySelector withObject:name];

#pragma clang diagnostic pop

注意我們是如何在相關(guān)代碼上下文中用 pragma 停用 -Warc-performSelector-leaks 檢查的录淡。這確保我們沒有全局禁用啤斗。如果全局禁用,可能會導(dǎo)致錯(cuò)誤赁咙。

全部的選項(xiàng)可以在 The Clang User's Manual 找到并且學(xué)習(xí)。

忽略沒用使用變量的編譯警告

告訴你申明的變量它將不會被使用免钻,這種做法很有用彼水。大多數(shù)情況下,你希望移除這些引用來(稍微地)提高性能极舔,但是有時(shí)候你希望保留它們凤覆。為什么?或許它們以后有用拆魏,或者有些特性只是暫時(shí)移除盯桦。無論如何慈俯,一個(gè)消除這些警告的好方法是用相關(guān)語句進(jìn)行注解,使用 #pragma unused():

- (NSInteger)giveMeFive
{
    NSString *foo;
    #pragma unused (foo)

    return 5;
}

現(xiàn)在你的代碼不用任何編譯警告了拥峦。注意你的 pragma 需要標(biāo)記到問題代碼之下贴膘。

明確編譯器警告和錯(cuò)誤

編譯器是一個(gè)機(jī)器人,它會標(biāo)記你代碼中被 Clang 規(guī)則定義為錯(cuò)誤的地方略号。但是刑峡,你總是比 Clang 更聰明。通常玄柠,你會發(fā)現(xiàn)一些討厭的代碼會導(dǎo)致這個(gè)問題突梦,但是暫時(shí)卻解決不了。你可以這樣明確一個(gè)錯(cuò)誤:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
    #error Whoa, buddy, you need to check for zero here!
    return (dividend / divisor);
}

類似的羽利,你可以這樣標(biāo)明一個(gè)警告

- (float)divide:(float)dividend by:(float)divisor
{
    #warning Dude, don't compare floating point numbers like this!
    if (divisor != 0.0) {
        return (dividend / divisor);
    }
    else {
        return NAN;
    }
}

字符串文檔

所有重要的方法宫患,接口,分類以及協(xié)議定義應(yīng)該有伴隨的注釋來解釋它們的用途以及如何使用这弧。更多的例子可以看 Google 代碼風(fēng)格指南中的 File and Declaration Comments娃闲。

簡而言之:有長的和短的兩種字符串文檔。

短文檔適用于單行的文件当宴,包括注釋斜杠畜吊。它適合簡短的函數(shù),特別是(但不僅僅是)非 public 的 API:

// Return a user-readable form of a Frobnozz, html-escaped.

文本應(yīng)該用一個(gè)動詞 ("return") 而不是 "returns" 這樣的描述户矢。

如果描述超過一行玲献,應(yīng)改用長字符串文檔:

  • /**開始
  • 換行寫一句總結(jié)的話,以?或者!或者.結(jié)尾梯浪。
  • 空一行
  • 在與第一行對齊的位置開始寫剩下的注釋
  • 最后用*/結(jié)束捌年。
/**
 This comment serves to demonstrate the format of a docstring.

 Note that the summary line is always at most one line long, and
 after the opening block comment, and each line of text is preceded
 by a single space.
*/

一個(gè)函數(shù)必須有一個(gè)字符串文檔,除非它符合下面的所有條件:

  • 非公開
  • 很短
  • 顯而易見

字符串文檔應(yīng)該描述函數(shù)的調(diào)用符號和語義挂洛,而不是它如何實(shí)現(xiàn)礼预。

注釋

當(dāng)它需要的時(shí)候,注釋應(yīng)該用來解釋特定的代碼做了什么虏劲。所有的注釋必須被持續(xù)維護(hù)或者干脆就刪掉托酸。

塊注釋應(yīng)該被避免,代碼本身應(yīng)該盡可能就像文檔一樣表示意圖柒巫,只需要很少的打斷注釋励堡。 例外: 這不能適用于用來產(chǎn)生文檔的注釋

頭文檔

一個(gè)類的文檔應(yīng)該只在 .h 文件里用 Doxygen/AppleDoc 的語法書寫。 方法和屬性都應(yīng)該提供文檔堡掏。

**例子: **

/**
 *  Designated initializer.
 *
 *  @param  store  The store for CRUD operations.
 *  @param  searchService The search service used to query the store.
 *
 *  @return A ZOCCRUDOperationsStore object.
 */
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store
                          searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;

對象間的通訊

對象之間需要通信应结,這也是所有軟件的基礎(chǔ)。再非凡的軟件也需要通過對象通信來完成復(fù)雜的目標(biāo)。本章將深入討論一些設(shè)計(jì)概念鹅龄,以及如何依據(jù)這些概念來設(shè)計(jì)出良好的架構(gòu)揩慕。

Block

Block 是 Objective-C 版本的 lambda 或者 closure(閉包)。

使用 block 定義異步接口:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion;

當(dāng)你定義一個(gè)類似上面的接口的時(shí)候扮休,盡量使用一個(gè)單獨(dú)的 block 作為接口的最后一個(gè)參數(shù)迎卤。把需要提供的數(shù)據(jù)和錯(cuò)誤信息整合到一個(gè)單獨(dú) block 中,比分別提供成功和失敗的 block 要好肛炮。

以下是你應(yīng)該這樣做的原因:

  • 通常這成功處理和失敗處理會共享一些代碼(比如讓一個(gè)進(jìn)度條或者提示消失)止吐;
  • Apple 也是這樣做的,與平臺一致能夠帶來一些潛在的好處侨糟;
  • block 通常會有多行代碼碍扔,如果不作為最后一個(gè)參數(shù)放在后面的話,會打破調(diào)用點(diǎn)秕重;
  • 使用多個(gè) block 作為參數(shù)可能會讓調(diào)用看起來顯得很笨拙不同,并且增加了復(fù)雜性。

看上面的方法溶耘,完成處理的 block 的參數(shù)很常見:第一個(gè)參數(shù)是調(diào)用者希望獲取的數(shù)據(jù)二拐,第二個(gè)是錯(cuò)誤相關(guān)的信息。這里需要遵循以下兩點(diǎn):

  • objects 不為 nil凳兵,則 error 必須為 nil
  • objects 為 nil百新,則 error 必須不為 nil

因?yàn)檎{(diào)用者更關(guān)心的是實(shí)際的數(shù)據(jù),就像這樣:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

此外庐扫,Apple 提供的一些同步接口在成功狀態(tài)下向 error 參數(shù)(如果非 NULL) 寫入了垃圾值饭望,所以檢查 error 的值可能出現(xiàn)問題。

深入 Block

一些關(guān)鍵點(diǎn):

  • block 是在棧上創(chuàng)建的
  • block 可以復(fù)制到堆上
  • Block會捕獲棧上的變量(或指針)形庭,將其復(fù)制為自己私有的const(變量)铅辞。
  • (如果在Block中修改Block塊外的)棧上的變量和指針,那么這些變量和指針必須用__block關(guān)鍵字申明(譯者注:否則就會跟上面的情況一樣只是捕獲他們的瞬時(shí)值)萨醒。

如果 block 沒有在其他地方被保持斟珊,那么它會隨著棧生存并且當(dāng)棧幀(stack frame)返回的時(shí)候消失。僅存在于棧上時(shí)富纸,block對對象訪問的內(nèi)存管理和生命周期沒有任何影響囤踩。

如果 block 需要在棧幀返回的時(shí)候存在,它們需要明確地被復(fù)制到堆上晓褪,這樣高职,block 會像其他 Cocoa 對象一樣增加引用計(jì)數(shù)。當(dāng)它們被復(fù)制的時(shí)候辞州,它會帶著它們的捕獲作用域一起,retain 他們所有引用的對象寥粹。

如果一個(gè) block引用了一個(gè)棧變量或指針变过,那么這個(gè)block初始化的時(shí)候會擁有這個(gè)變量或指針的const副本埃元,所以(被捕獲之后再在棧中改變這個(gè)變量或指針的值)是不起作用的。(譯者注:所以這時(shí)候我們在block中對這種變量進(jìn)行賦值會編譯報(bào)錯(cuò):Variable is not assignable(missing __block type specifier)媚狰,因?yàn)樗麄兪歉北径沂莄onst的.具體見下面的例程)岛杀。

當(dāng)一個(gè) block 被復(fù)制后,__block 聲明的棧變量的引用被復(fù)制到了堆里崭孤,復(fù)制完成之后类嗤,無論是棧上的block還是剛剛產(chǎn)生在堆上的block(棧上block的副本)都會引用該變量在堆上的副本。

(下面代碼是譯者加的)

    ...
    CGFloat blockInt = 10;
    void (^playblock)(void) = ^{
        NSLog(@"blockInt = %zd", blockInt);
    };
    blockInt ++;
    playblock();
    ...

    //結(jié)果為:blockInt = 10

用 LLDB 來展示 block 是這樣子的:

最重要的事情是 __block 聲明的變量和指針在 block 里面是作為顯示操作真實(shí)值/對象的結(jié)構(gòu)來對待的辨宠。

block 在 Objective-C 的 runtime(運(yùn)行時(shí)) 里面被當(dāng)作一等公民對待:他們有一個(gè) isa 指針遗锣,一個(gè)類也是用 isa 指針在Objective-C 運(yùn)行時(shí)來訪問方法和存儲數(shù)據(jù)的。在非 ARC 環(huán)境肯定會把它搞得很糟糕嗤形,并且懸掛指針會導(dǎo)致 crash精偿。__block 僅僅對 block 內(nèi)的變量起作用,它只是簡單地告訴 block:

嗨赋兵,這個(gè)指針或者原始的類型依賴它們在的棧笔咽。請用一個(gè)棧上的新變量來引用它。我是說霹期,請對它進(jìn)行雙重解引用叶组,不要 retain 它。
謝謝历造,哥們甩十。

如果在定義之后但是 block 沒有被調(diào)用前,對象被釋放了帕膜,那么 block 的執(zhí)行會導(dǎo)致 crash枣氧。 __block 變量不會在 block 中被持有,最后... 指針垮刹、引用达吞、解引用以及引用計(jì)數(shù)變得一團(tuán)糟。

self 的循環(huán)引用

當(dāng)使用代碼塊和異步分發(fā)的時(shí)候荒典,要注意避免引用循環(huán)酪劫。 總是使用 weak 來引用對象,避免引用循環(huán)寺董。(譯者注:這里更為優(yōu)雅的方式是采用影子變量@weakify/@strongify 這里有更為詳細(xì)的說明) 此外覆糟,把持有 block 的屬性設(shè)置為 nil (比如 self.completionBlock = nil) 是一個(gè)好的實(shí)踐。它會打破 block 捕獲的作用域帶來的引用循環(huán)遮咖。

例子:

__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
}];

不要這樣:

[self executeBlock:^(NSData *data, NSError *error) {
    [self doSomethingWithData:data];
}];

多個(gè)語句的例子:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];

不要這樣:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
    [weakSelf doSomethingWithData:data];
}];

你應(yīng)該把這兩行代碼作為 snippet 加到 Xcode 里面并且總是這樣使用它們滩字。

__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;

這里我們來討論下 block 里面的 self 的 __weak__strong 限定詞的一些微妙的地方。簡而言之,我們可以參考 self 在 block 里面的三種不同情況麦箍。

  1. 直接在 block 里面使用關(guān)鍵詞 self
  2. 在 block 外定義一個(gè) __weak 的 引用到 self漓藕,并且在 block 里面使用這個(gè)弱引用
  3. 在 block 外定義一個(gè) __weak 的 引用到 self,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用挟裂。

方案 1. 直接在 block 里面使用關(guān)鍵詞 self

如果我們直接在 block 里面用 self 關(guān)鍵字享钞,對象會在 block 的定義時(shí)候被 retain,(實(shí)際上 block 是 copied 但是為了簡單我們可以忽略這個(gè))诀蓉。一個(gè) const 的對 self 的引用在 block 里面有自己的位置并且它會影響對象的引用計(jì)數(shù)栗竖。如果這個(gè)block被其他的類使用并且(或者)彼此間傳來傳去,我們可能想要在 block 中保留 self渠啤,就像其他在 block 中使用的對象一樣. 因?yàn)樗麄兪莃lock執(zhí)行所需要的.

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

沒啥大不了狐肢。但是如果通過一個(gè)屬性中的 self 保留 了這個(gè) block(就像下面的例程一樣),對象( self )保留了 block 會怎么樣呢?

self.completionHandler = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這就是有名的 retain cycle, 并且我們通常應(yīng)該避免它埃篓。這種情況下我們收到 CLANG 的警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 里面發(fā)現(xiàn)了 `self` 的強(qiáng)引用处坪,可能會導(dǎo)致循環(huán)引用)

所以 __weak 就有用武之地了。

方案 2. 在 block 外定義一個(gè) __weak 的 引用到 self架专,并且在 block 里面使用這個(gè)弱引用

這樣會避免循壞引用同窘,也是通常情況下我們的block作為類的屬性被self retain 的時(shí)候會做的。

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    NSLog(@"%@", weakSelf);
};

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這個(gè)情況下 block 沒有 retain 對象并且對象在屬性里面 retain 了 block 部脚。所以這樣我們能保證了安全的訪問 self想邦。 不過糟糕的是,它可能被設(shè)置成 nil 的委刘。問題是:如何讓 self 在 block 里面安全地被銷毀丧没。

考慮這么個(gè)情況:block 作為屬性(property)賦值的結(jié)果,從一個(gè)對象被復(fù)制到另一個(gè)對象(如 myController)锡移,在這個(gè)復(fù)制的 block 執(zhí)行之前呕童,前者(即之前的那個(gè)對象)已經(jīng)被解除分配。

下面的更有意思淆珊。

方案 3. 在 block 外定義一個(gè) __weak 的 引用到 self夺饲,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用

你可能會想,首先施符,這是避免 retain cycle 警告的一個(gè)技巧往声。

這不是重點(diǎn),這個(gè) self 的強(qiáng)引用是在block 執(zhí)行時(shí) 被創(chuàng)建的戳吝,但是否使用 self 在 block 定義時(shí)就已經(jīng)定下來了浩销, 因此self (在block執(zhí)行時(shí)) 會被 retain.

Apple 文檔 中表示 "為了 non-trivial cycles ,你應(yīng)該這樣" :

MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

首先听哭,我覺得這個(gè)例子看起來是錯(cuò)誤的慢洋。如果 block 本身在 completionHandler 屬性中被 retain 了塘雳,那么 self 如何被 delloc 和在 block 之外賦值為 nil 呢? completionHandler 屬性可以被聲明為 assign 或者 unsafe_unretained 的,來允許對象在 block 被傳遞之后被銷毀且警。

我不能理解這樣做的理由粉捻,如果其他對象需要這個(gè)對象(self),block 被傳遞的時(shí)候應(yīng)該 retain 對象斑芜,所以 block 應(yīng)該不被作為屬性存儲。這種情況下不應(yīng)該用 __weak/__strong

總之祟霍,其他情況下杏头,希望 weakSelf 變成 nil 的話,就像第二種情況解釋那么寫(在 block 之外定義一個(gè)弱應(yīng)用并且在 block 里面使用)沸呐。

還有醇王,Apple的 "trivial block" 是什么呢。我們的理解是 trivial block 是一個(gè)不被傳送的 block 崭添,它在一個(gè)良好定義和控制的作用域里面寓娩,weak 修飾只是為了避免循環(huán)引用。

雖然有 Kazuki Sakamoto 和 Tomohiko Furumoto) 討論的 在線 參考, Matt Galloway 的 (Effective Objective-C 2.0Pro Multithreading and Memory Management for iOS and OS X 呼渣,大多數(shù)開發(fā)者始終沒有弄清楚概念棘伴。

在 block 內(nèi)用強(qiáng)引用的優(yōu)點(diǎn)是,搶占執(zhí)行的時(shí)候的魯棒性屁置。在 block 執(zhí)行的時(shí)候, 再次溫故下上面的三個(gè)例子:

方案 1. 直接在 block 里面使用關(guān)鍵詞 self

如果 block 被屬性 retain焊夸,self 和 block 之間會有一個(gè)循環(huán)引用并且它們不會再被釋放。如果 block 被傳送并且被其他的對象 copy 了蓝角,self 在每一個(gè) copy 里面被 retain

方案 2. 在 block 外定義一個(gè) __weak 的 引用到 self阱穗,并且在 block 里面使用這個(gè)弱引用

不管 block 是否通過屬性被 retain ,這里都不會發(fā)生循環(huán)引用使鹅。如果 block 被傳遞或者 copy 了揪阶,在執(zhí)行的時(shí)候,weakSelf 可能已經(jīng)變成 nil患朱。

block 的執(zhí)行可以搶占鲁僚,而且對 weakSelf 指針的調(diào)用時(shí)序不同可以導(dǎo)致不同的結(jié)果(如:在一個(gè)特定的時(shí)序下 weakSelf 可能會變成nil)。

__weak typeof(self) weakSelf = self;
dispatch_block_t block =  ^{
    [weakSelf doSomething]; // weakSelf != nil
    // preemption, weakSelf turned nil
    [weakSelf doSomethingElse]; // weakSelf == nil
};

方案 3. 在 block 外定義一個(gè) __weak 的 引用到 self麦乞,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用蕴茴。

不管 block 是否通過屬性被 retain ,這里也不會發(fā)生循環(huán)引用姐直。如果 block 被傳遞到其他對象并且被復(fù)制了鸦采,執(zhí)行的時(shí)候赊淑,weakSelf 可能被nil,因?yàn)閺?qiáng)引用被賦值并且不會變成nil的時(shí)候,我們確保對象 在 block 調(diào)用的完整周期里面被 retain了,如果搶占發(fā)生了撑蚌,隨后的對 strongSelf 的執(zhí)行會繼續(xù)并且會產(chǎn)生一樣的值。如果 strongSelf 的執(zhí)行到 nil,那么在 block 不能正確執(zhí)行前已經(jīng)返回了科展。

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      [strongSelf doSomething]; // strongSelf != nil
      // preemption, strongSelf still not nil(搶占的時(shí)候,strongSelf 還是非 nil 的)
      [strongSelf doSomethingElse]; // strongSelf != nil
    }
    else {
        // Probably nothing...
        return;
    }
};

在ARC條件中糠雨,如果嘗試用 -> 符號訪問一個(gè)實(shí)例變量才睹,編譯器會給出非常清晰的錯(cuò)誤信息:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個(gè) __weak 指針的解引用不允許的,因?yàn)榭赡茉诟倯B(tài)條件里面變成 null, 所以先把他定義成 strong 的屬性)

可以用下面的代碼展示

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    id localVal = weakSelf->someIVar;
};

在最后

  • 方案 1: 只能在 block 不是作為一個(gè) property 的時(shí)候使用甘邀,否則會導(dǎo)致 retain cycle琅攘。

  • 方案 2: 當(dāng) block 被聲明為一個(gè) property 的時(shí)候使用。

  • 方案 3: 和并發(fā)執(zhí)行有關(guān)松邪。當(dāng)涉及異步的服務(wù)的時(shí)候坞琴,block 可以在之后被執(zhí)行,并且不會發(fā)生關(guān)于 self 是否存在的問題逗抑。

委托和數(shù)據(jù)源

委托模式 是 Apple 的框架里面使用廣泛的模式剧辐,同時(shí)它是四人幫的書“設(shè)計(jì)模式”中的重要模式之一。委托代理模式是單向的邮府,消息的發(fā)送方(委托方)需要知道接收方(代理方)是誰荧关,反過來就沒有必要了。對象之間耦合較松挟纱,發(fā)送方僅需知道它的代理方是否遵守相關(guān) protocol 即可羞酗。

本質(zhì)上,委托代理模式僅需要代理方提供一些回調(diào)方法紊服,即代理方需要實(shí)現(xiàn)一系列空返回值的方法檀轨。

不幸的是 Apple 的 API 并沒有遵守這個(gè)原則,開發(fā)者也效仿 Apple 進(jìn)入了一個(gè)誤區(qū)欺嗤。典型的例子就是 UITableViewDelegate 協(xié)議参萄。

它的一些方法返回 void 類型,就像我們所說的回調(diào):

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;

但是其他的就不是那么回事:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;

當(dāng)委托者詢問代理者一些信息的時(shí)候煎饼,這就暗示著信息是從代理者流向委托者而非相反的過程讹挎。 這(譯者注:委托者 ==Data==> 代理者)是概念性的不同,須用另一個(gè)新的名字來描述這種模式:數(shù)據(jù)源模式吆玖。

可能有人會說 Apple 有一個(gè) UITableViewDataSouce protocol 來做這個(gè)(雖然使用委托模式的名字)筒溃,但是實(shí)際上它的方法是用來提供真實(shí)的數(shù)據(jù)應(yīng)該如何被展示的信息的。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

此外沾乘,以上兩個(gè)方法 Apple 混合了展示層和數(shù)據(jù)層怜奖,這顯的非常糟糕,但是很少的開發(fā)者感到糟糕翅阵。而且我們在這里把空返回值和非空返回值的方法都天真地叫做委托方法歪玲。

為了分離概念迁央,我們應(yīng)該這樣做:

  • 委托模式(delegate pattern):事件發(fā)生的時(shí)候,委托者需要通知代理者滥崩。
  • 數(shù)據(jù)源模式(datasource pattern): 委托者需要從數(shù)據(jù)源對象拉取數(shù)據(jù)岖圈。

這個(gè)是實(shí)際的例子:

@class ZOCSignUpViewController;

@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end


@interface ZOCSignUpViewController : UIViewController

@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;

@end

代理方法必須以調(diào)用者(即委托者)作為第一個(gè)參數(shù),就像上面的例子一樣钙皮。否則代理者無法區(qū)分不同的委托者實(shí)例蜂科。換句話說,調(diào)用者(委托者)沒有被傳遞給代理短条,那就沒有方法讓代理處理兩個(gè)不同的委托者崇摄,所以下面這種寫法人神共怒:

- (void)calculatorDidCalculateValue:(CGFloat)value;

默認(rèn)情況下,代理者需要實(shí)現(xiàn) protocol 的方法慌烧。可以用@required@optional 關(guān)鍵字來標(biāo)記方法是否是必要的還是可選的(默認(rèn)是 @required: 必需的)鸠儿。

@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *)dict;
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

對于可選的方法屹蚊,委托者必須在發(fā)送消息前檢查代理是否確實(shí)實(shí)現(xiàn)了特定的方法(否則會 crash):

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
    [self.delegate signUpViewControllerDidPressSignUpButton:self];
}

繼承

有時(shí)候你可能需要重載代理方法〗浚考慮有兩個(gè) UIViewController 子類的情況:UIViewControllerA 和 UIViewControllerB汹粤,有下面的類繼承關(guān)系。

UIViewControllerB < UIViewControllerA < UIViewController

UIViewControllerA 遵從 UITableViewDelegate 并且實(shí)現(xiàn)了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

你可能會想要在 UIViewControllerB 中提供一個(gè)不同的實(shí)現(xiàn)田晚,這個(gè)實(shí)現(xiàn)可能是這樣子的:


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

但是如果超類(UIViewControllerA)沒有實(shí)現(xiàn)這個(gè)方法呢嘱兼?此時(shí)調(diào)用[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]方法,將使用 NSObject 的實(shí)現(xiàn)贤徒,在 self 上下文深入查找并且明確 self 實(shí)現(xiàn)了這個(gè)方法(因?yàn)?UITableViewControllerA 遵從 UITableViewDelegate)芹壕,但是應(yīng)用將在下一行發(fā)生崩潰,并提示如下錯(cuò)誤信息:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'

*** 由于未捕獲異常 `NSInvalidArgumentException(無效的參數(shù)異常)`導(dǎo)致應(yīng)用終止接奈,理由是:向?qū)嵗?ox8d82820 發(fā)送了無法識別的 selector `- [UIViewControllerB tableView:heightForRowAtIndexPath:]`

這種情況下我們需要來詢問特定的類實(shí)例是否可以響應(yīng)對應(yīng)的 selector踢涌。下面的代碼提供了一個(gè)小技巧:


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

就像上面丑陋的代碼,通常它會是更好的設(shè)計(jì)架構(gòu)的方式序宦,因?yàn)檫@種方式代理方法不需要被重寫睁壁。

多重委托

多重委托是一個(gè)非常基礎(chǔ)的概念互捌,但是潘明,大多數(shù)開發(fā)者對此非常不熟悉而使用 NSNotifications。就像你可能注意到的秕噪,委托和數(shù)據(jù)源是對象之間的通訊模式钳降,但是只涉及兩個(gè)對象:委托者和委托。

數(shù)據(jù)源模式強(qiáng)制一對一的關(guān)系巢价,當(dāng)發(fā)送者請求信息時(shí)有且只能有一個(gè)對象來響應(yīng)牲阁。對于代理模式而言這會有些不同固阁,我們有足夠的理由要去實(shí)現(xiàn)很多代理者等待(唯一委托者的)回調(diào)的場景。

一些情況下至少有兩個(gè)對象對特定委托者的回調(diào)感興趣城菊,而后者(即委托者)需要知道他的所有代理备燃。這種方法在分布式系統(tǒng)下更為適用并且廣泛使用于大型軟件的復(fù)雜信息流程中。

多重委托可以用很多方式實(shí)現(xiàn)凌唬,但讀者更在乎找到適合自己的個(gè)人實(shí)現(xiàn)并齐。Luca Bernardi 在他的 LBDelegateMatrioska中提供了上述范式的一個(gè)非常簡潔的實(shí)現(xiàn)。

這里給出一個(gè)基本的實(shí)現(xiàn),方便你更好地理解這個(gè)概念客税。即使在Cocoa中也有一些在數(shù)據(jù)結(jié)構(gòu)中保存 weak 引用來避免 引用循環(huán)的方法况褪, 這里我們使用一個(gè)類來保留代理對象的 weak 引用(就像單一代理那樣):

@interface ZOCWeakObject : NSObject

@property (nonatomic, readonly, weak) id object;
//譯者注:這里原文并沒有很好地實(shí)踐自己在本書之前章節(jié)所討論的關(guān)于property屬性修飾符的
//人體工程學(xué)法則: 從左到右: 原子性 ===》 讀寫權(quán)限 (別名) ===》 內(nèi)存管理權(quán)限符

+ (instancetype)weakObjectWithObject:(id)object;
- (instancetype)initWithObject:(id)object;

@end
@interface ZOCWeakObject ()
@property (nonatomic, weak) id object;
@end

@implementation ZOCWeakObject

+ (instancetype)weakObjectWithObject:(id)object {
    return [[[self class] alloc] initWithObject:object];
}

- (instancetype)initWithObject:(id)object {
    if ((self = [super init])) {
        _object = object;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[object class]]) {
        return NO;
    }

    return [self isEqualToWeakObject:(ZOCWeakObject *)object];
}

- (BOOL)isEqualToWeakObject:(ZOCWeakObject *)object {
    if (!object) {
        return NO;
    }

    BOOL objectsMatch = [self.object isEqual:object.object];
    return objectsMatch;
}

- (NSUInteger)hash {
    return [self.object hash];
}

@end

使用 weak 對象來實(shí)現(xiàn)多重代理的簡單組件:

@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end

@interface ZOCGeneralService : NSObject
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate;
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate;
@end

@interface ZOCGeneralService ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
@implementation ZOCGeneralService
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate {
    if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
        [self.delegates addObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
    }
}

- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate {
    if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
        [self.delegates removeObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
    }
}

- (void)_notifyDelegates {
    ...
    for (ZOCWeakObject *object in self.delegates) {
        if (object.object) {
            if ([object.object respondsToSelector:@selector(generalService:didRetrieveEntries:)]) {
                [object.object generalService:self didRetrieveEntries:entries];
            }
        }
    }
}

@end

registerDelegate:deregisterDelegate: 方法的幫助下,連接/解除組件之間的聯(lián)系很簡單:在某些時(shí)間點(diǎn)上更耻,如果代理不需要接收委托者的回調(diào)测垛,僅僅需要'unsubscribe'.

當(dāng)不同的 view 等待同一個(gè)回調(diào)來更新界面展示的時(shí)候,這很有用:如果 view 只是暫時(shí)隱藏(但是仍然存在)秧均,它僅僅需要取消對回調(diào)的訂閱食侮。

面向切面編程

Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區(qū)內(nèi)沒有那么有名目胡,但是 AOP 在運(yùn)行時(shí)可以有巨大威力锯七。 但是因?yàn)闆]有事實(shí)上的標(biāo)準(zhǔn),Apple 也沒有開箱即用的提供誉己,也顯得不重要眉尸,開發(fā)者都不怎么考慮它。

引用 Aspect Oriented Programming 維基頁面:

An aspect can alter the behavior of the base code (the non-aspect part of a program) by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches). (一個(gè)切面可以通過在多個(gè) join points 中附加的行為來改變基礎(chǔ)代碼的行為(程序的非切面的部分) )

在 Objective-C 的世界里巨双,這意味著使用運(yùn)行時(shí)的特性來為指定的方法追加 切面 噪猾。切面所附加的行為可以是這樣的:

  • 在類的特定方法調(diào)用前運(yùn)行特定的代碼
  • 在類的特定方法調(diào)用后運(yùn)行特定的代碼
  • 增加代碼來替代原來的類的方法的實(shí)現(xiàn)

有很多方法可以達(dá)成這些目的,但是我們沒有深入挖掘炉峰,不過它們主要都是利用了運(yùn)行時(shí)畏妖。 Peter Steinberger 寫了一個(gè)庫,Aspects 完美地適配了 AOP 的思路疼阔。我們發(fā)現(xiàn)它值得信賴以及設(shè)計(jì)得非常優(yōu)秀戒劫,所以我們就在這邊作為一個(gè)簡單的例子。

對于所有的 AOP庫婆廊,這個(gè)庫用運(yùn)行時(shí)做了一些非逞赶福酷的魔法,可以替換或者增加一些方法(比 method swizzling 技術(shù)更有技巧性)

Aspect 的 API 有趣并且非常強(qiáng)大:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

比如淘邻,下面的代碼會對于執(zhí)行 MyClass 類的 myMethod: (實(shí)例或者類的方法) 執(zhí)行塊參數(shù)茵典。


[MyClass aspect_hookSelector:@selector(myMethod:)
                 withOptions:AspectPositionAfter
                  usingBlock:^(id<AspectInfo> aspectInfo) {
            ...
        }
                       error:nil];

換一句話說:任意的 MyClass 類型的對象(或者是類型本身當(dāng)這個(gè) @selector 方法為類方法時(shí))的 @selector 方法執(zhí)行完后,就會執(zhí)行這個(gè)代碼中塊參數(shù)所提供的代碼宾舅。

我們?yōu)?MyClass 類的 myMethod: 方法增加了切面统阿。

通常 AOP 被用來實(shí)現(xiàn)橫向切面彩倚。統(tǒng)計(jì)與日志就是一個(gè)完美的例子。

下面的例子里面扶平,我們會用AOP用來進(jìn)行統(tǒng)計(jì)帆离。統(tǒng)計(jì)是iOS項(xiàng)目里面一個(gè)熱門的特性,有很多選擇比如 Google Analytics, Flurry, MixPanel, 等等.

大部分統(tǒng)計(jì)框架都有教程來指導(dǎo)如何追蹤特定的界面和事件结澄,包括在每一個(gè)類里寫幾行代碼哥谷。

在 Ray Wenderlich 的博客里有 文章 和一些示例代碼,通過在你的 view controller 里面加入 Google Analytics 進(jìn)行統(tǒng)計(jì)麻献。


- (void)logButtonPress:(UIButton *)button {
    id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
    [tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"UX"
                                                          action:@"touch"
                                                           label:[button.titleLabel text]
                                                           value:nil] build]];
}

上面的代碼在按鈕點(diǎn)擊的時(shí)候發(fā)送了特定的上下文事件们妥。但是當(dāng)你想追蹤屏幕的時(shí)候會變得很糟。


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
    [tracker set:kGAIScreenName value:@"Stopwatch"];
    [tracker send:[[GAIDictionaryBuilder createAppView] build]];
}

對于大部分有經(jīng)驗(yàn)的iOS工程師勉吻,這看起來不是很好的代碼监婶。我們讓 view controller 變得更糟糕了。因?yàn)槲覀兗尤肓私y(tǒng)計(jì)事件的代碼齿桃,但是它不是 view controller 的職能压储。你可以反駁,因?yàn)槟阃ǔS刑囟ǖ膶ο髞碡?fù)責(zé)統(tǒng)計(jì)追蹤源譬,并且你將代碼注入了 view controller ,但是無論你隱藏邏輯孕似,問題仍然存在 :你最后還是在viewDidAppear: 后插入了代碼踩娘。

我們可以在類的 viewDidAppear: 方法上使用 AOP 來追蹤屏幕,并且我們可以使用同樣的方法在其他我們感興趣的方法上添加事件追蹤喉祭。比如當(dāng)用戶點(diǎn)擊某個(gè)按鈕時(shí)(比如:一般調(diào)用對應(yīng)的 IBAction).

方法很簡潔且不具侵入性:

  • view controller 不會被不屬于它的代碼污染
  • 為所有加入到我們代碼的切面指定一個(gè) SPOC 文件 (single point of customization)提供了可能
  • SPOC 應(yīng)該在 App 剛開始啟動的時(shí)候用來添加切面
  • 如果SPOC文件異常,至少有一個(gè) selector 或者 類 識別不出來养渴,應(yīng)用將會在啟動時(shí)崩潰(對我們來說這很酷).
  • 公司負(fù)責(zé)統(tǒng)計(jì)的團(tuán)隊(duì)通常會提供統(tǒng)計(jì)文檔,羅列出需要追蹤的事件泛烙。這個(gè)文檔可以很容易映射到一個(gè) SPOC 文件理卑。
  • 追蹤邏輯抽象化之后,擴(kuò)展到很多其他統(tǒng)計(jì)框架會很方便
  • 對于屏幕視圖蔽氨,對于需要定義 selector 的方法藐唠,只需要在 SPOC 文件修改相關(guān)的類(相關(guān)的切面會加入到 viewDidAppear: 方法)。如果要同時(shí)發(fā)送屏幕視圖和事件鹉究,需要(依靠統(tǒng)計(jì)提供方)提供一個(gè)追蹤的標(biāo)示或者可能還需要提供其他的元信息宇立。

我們可能希望一個(gè) SPOC 文件類似下面的(同樣的一個(gè) .plist 文件會適配)


NSDictionary *analyticsConfiguration()
{
    return @{
        @"trackedScreens" : @[
            @{
                @"class" : @"ZOCMainViewController",
                @"label" : @"Main screen"
                }
             ],
        @"trackedEvents" : @[
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginViewFetchedUserInfo:user:",
                @"label" : @"Login with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginViewShowingLoggedOutUser:",
                @"label" : @"Logout with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginView:handleError:",
                @"label" : @"Login error with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"shareButtonPressed:",
                @"label" : @"Share button"
                }
             ]
    };
}

提及的架構(gòu)托管 在 Github 的EF Education First 中.


- (void)setupWithConfiguration:(NSDictionary *)configuration
{
    // screen views tracking
    for (NSDictionary *trackedScreen in configuration[@"trackedScreens"]) {
        Class clazz = NSClassFromString(trackedScreen[@"class"]);

        [clazz aspect_hookSelector:@selector(viewDidAppear:)
                       withOptions:AspectPositionAfter
                        usingBlock:^(id<AspectInfo> aspectInfo) {
               dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                          ^{
                          NSString *viewName = trackedScreen[@"label"];
                        [tracker trackScreenHitWithName:viewName];
                      });
                  }
                            error:nil];

    }

    // events tracking
    for (NSDictionary *trackedEvents in configuration[@"trackedEvents"]) {
        Class clazz = NSClassFromString(trackedEvents[@"class"]);
        SEL selektor = NSSelectorFromString(trackedEvents[@"selector"]);

        [clazz aspect_hookSelector:selektor
                       withOptions:AspectPositionAfter
                        usingBlock:^(id<AspectInfo> aspectInfo) {
               dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                      ^{
                            UserActivityButtonPressedEvent *buttonPressEvent = \
                                [UserActivityButtonPressedEvent \
                                    eventWithLabel:trackedEvents[@"label"]];
                            [tracker trackEvent:buttonPressEvent];
                      });
                }
                       error:nil];

    }
}

參考資料

這里有一些和風(fēng)格指南有關(guān)的蘋果的文檔:

其他:

  • Objective-Clean: an attempt to write a standard for writing Objective-C code with Xcode integration;
  • Uncrustify: source code beautifier.

其他的 Objective-C 風(fēng)格指南

這里有一些和風(fēng)格指南有關(guān)的蘋果的文檔。如果有一些本書沒有涉獵的地方自赔,你或許能在這些之中找到詳細(xì)說明妈嘹。

來自 Apple 的:

來自社區(qū)的:

[1]: https://github.com/NYTimes/objective-c-style-guide/issues/6)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市绍妨,隨后出現(xiàn)的幾起案子润脸,更是在濱河造成了極大的恐慌柬脸,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毙驯,死亡現(xiàn)場離奇詭異倒堕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尔苦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門涩馆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人允坚,你說我怎么就攤上這事魂那。” “怎么了稠项?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵涯雅,是天一觀的道長。 經(jīng)常有香客問我展运,道長活逆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任拗胜,我火速辦了婚禮蔗候,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埂软。我一直安慰自己锈遥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布勘畔。 她就那樣靜靜地躺著所灸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炫七。 梳的紋絲不亂的頭發(fā)上爬立,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音万哪,去河邊找鬼侠驯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛奕巍,可吹牛的內(nèi)容都是我干的陵霉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼伍绳,長吁一口氣:“原來是場噩夢啊……” “哼踊挠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤效床,失蹤者是張志新(化名)和其女友劉穎睹酌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剩檀,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡憋沿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沪猴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辐啄。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖运嗜,靈堂內(nèi)的尸體忽然破棺而出壶辜,到底是詐尸還是另有隱情,我是刑警寧澤担租,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布砸民,位于F島的核電站,受9級特大地震影響奋救,放射性物質(zhì)發(fā)生泄漏岭参。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一尝艘、第九天 我趴在偏房一處隱蔽的房頂上張望演侯。 院中可真熱鬧,春花似錦背亥、人聲如沸蚌本。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舷嗡,卻和暖如春轴猎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背进萄。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工捻脖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人中鼠。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓可婶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親援雇。 傳聞我的和親對象是個(gè)殘疾皇子矛渴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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