天天看點

【編碼風格】禅與 Objective-C 程式設計藝術 (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)

本文來自:https://github.com/oa414/objc-zen-book-cn#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86  

錯誤處理

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

推薦:

<span style="font-size:14px;">NSError *error = nil;
if (![self trySomethingWithError:&error]) {
    // Handle Error
}</span>
           

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

常量

常量應該用 

static

 聲明,不要使用 

#define

,除非它就是明确作為一個宏來用的。

常量應該使用駝峰命名法,并且為了清楚,應該用相關的類名作為字首。

推薦

<span style="font-size:14px;">static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;</span></span>
           

常量應該在 interface 檔案中這樣被聲明:

<span style="font-size:14px;">extern NSString *const ZOCCacheControllerDidClearCacheNotification;</span>
           

并且應該在實作檔案中實作它的定義。

字面值

NSString

NSDictionary

NSArray

, 和 

NSNumber

 字面值應該用在任何建立不可變的執行個體對象。特别小心不要把 

nil

 放進 

NSArray

 和 

NSDictionary

 裡,這會導緻崩潰

例子:

<span style="font-size:14px;">NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;</span>
           

不要這樣:

<span style="font-size:14px;">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];</span>
           

對于那些可變的副本,我們推薦使用明确的如 

NSMutableArray

NSMutableString

 這些類。

Initializer和dealloc

init

 方法應該是這樣的結構:

<span style="font-size:14px;">- (instancetype)init
{
   self = [super init]; // call the designated initializer
   if (self) {
       // Custom initialization
   }
   return self;
}</span>
           

為什麼設定 

self

 為 

[super init]

 的傳回值,以及中間發生了什麼呢?這是一個十分有趣的話題。

讓我們後退一步:我們一直寫類似 

[[NSObject alloc] init]

 的表達式,而淡化了 

alloc

 和 

init

 的差別 。一個 Objective-C 的特性叫 兩步建立 。 這意味着申請配置設定記憶體和初始化是兩個分離的操作。

  • alloc

    表示對象配置設定記憶體,這個過程涉及配置設定足夠的可用記憶體來儲存對象,寫入

    isa

    指針,初始化 retain 的計數,并且初始化所有執行個體變量。
  • init

     是表示初始化對象,這意味着把對象轉換到了個可用的狀态。這通常是指把可用的值賦給了對象的執行個體變量。

alloc

 方法會傳回一個合法的沒有初始化的執行個體對象。每一個發送到執行個體的消息會被翻譯為

objc_msgSend()

 函數的調用,它的參數是指向 

alloc

 傳回的對象的、名為 

self

 的指針的。這樣之後 

self

 已經可以執行所有方法了。 為了完成兩步建立,第一個發送給新建立的執行個體的方法應該是約定俗成的 

init

 方法。注意在 

NSObject

 的 

init

 實作中,僅僅是傳回了 

self

關于 

init

 有一個另外的重要的約定:這個方法可以(并且應該)在不能成功完成初始化的時候傳回

nil

;初始化可能因為各種原因失敗,比如一個輸入的格式錯誤,或者未能成功初始化一個需要的對象。 這樣我們就了解了為什麼需要總是調用 

self = [super init]

。如果你的超類沒有成功初始化它自己,你必須假設你在一個沖突的狀态,并且在你的實作中不要處理你自己的初始化邏輯,同時傳回

nil

。如果你不是這樣做,你看你會得到一個不能用的對象,并且它的行為是不可預測的,最終可能會導緻你的 App 發生 crash。

重新給 

self

 指派同樣可以被 

init

 利用為在被調用的時候傳回不同的執行個體。一個例子是 類簇 或者其他的傳回相同的(不可變的)執行個體對象的 Cocoa 類。

Designated和Secondary Initializers

初始化模式

類簇(class cluster)

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

class cluster 的想法很簡單,你經常有一個抽象類在初始化期間處理資訊,經常作為一個構造器裡面的參數或者環境中讀取,來完成特定的邏輯并且執行個體化子類。這個"public facing" 應該知曉它的子類而且傳回适合的私有子類。

Class clusters 在 Apple 的Framework 中廣泛使用:一些明顯的例子比如  

NSNumber

 可以傳回不同類型給你的子類,取決于 數字類型如何提供 (Integer, Float, etc...) 或者 

NSArray

 傳回不同的最優存儲政策的子類。

一個經典的例子是如果你有為 iPad 和 iPhone 寫的一樣的 UIViewController 子類,但是在不同的裝置上有不同的行為。

<span style="font-size:14px;">@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</span></span>
           

上面的代碼的例子展示了如何建立一個類簇。

單例

如果可能,請盡量避免使用單例而是依賴注入。 然而,如果一定要用,請使用一個線程安全的模式來建立共享的執行個體。對于 GCD,用 

dispatch_once()

 函數就可以咯。

<span style="font-size:14px;">+ (instancetype)sharedInstance
{
   static id sharedInstance = nil;
   static dispatch_once_t onceToken = 0;
   dispatch_once(&onceToken, ^{
      sharedInstance = [[self alloc] init];
   });
   return sharedInstance;
}</span>
           

使用 dispatch_once(),來控制代碼同步,取代了原來的約定俗成的用法。

dispatch_once()

 的優點是,它更快,而且文法上更幹淨,因為dispatch_once()的意思就是 “把一些東西執行一次”,就像我們做的一樣。 這樣同時可以避免 possible and sometimes prolific crashes.

經典的使用單例對象的例子是一個裝置的 GPS 以及動作傳感器。即使單例對象可以被子類化,這個情況也可以十分有用。這個接口應該證明給出的類是趨向于使用單例的。然而,通常使用一個單獨的公開的 

sharedInstance

 類方法就夠了,并且不可寫的屬性也應該被暴露。

把單例作為一個對象的容器來在代碼或者應用層面上共享是糟糕和醜陋的,這是一個不好的設計。

屬性

你總應該用 getter 和 setter ,因為:

  • 使用 setter 會遵守定義的記憶體管理語義(

    strong

    weak

    copy

     etc...) ,這個在 ARC 之前就是相關的内容。舉個例子,

    copy

     屬性定義了每個時候你用 setter 并且傳送資料的時候,它會複制資料而不用額外的操作。
  • KVO 通知(

    willChangeValueForKey

    didChangeValueForKey

    ) 會被自動執行。
  • 更容易debug:你可以設定一個斷點在屬性聲明上并且斷點會在每次 getter / setter 方法調用的時候執行,或者你可以在自己的自定義 setter/getter 設定斷點。
  • 允許在一個單獨的地方為設定值添加額外的邏輯。

你應該傾向于用 getter:

  • 它是對未來的變化有擴充能力的(比如,屬性是自動生成的)。
  • 它允許子類化。
  • 更簡單的debug(比如,允許拿出一個斷點在 getter 方法裡面,并且看誰通路了特别的 getter
  • 它讓意圖更加清晰和明确:通過通路 ivar 

    _anIvar

     你可以明确的通路 

    self->_anIvar

    .這可能導緻問題。在 block 裡面通路 ivar (你捕捉并且 retain 了 self,即使你沒有明确的看到 self 關鍵詞)。
  • 它自動産生KVO 通知。
  • 在消息發送的時候增加的開銷是微不足道的。更多關于新年問題的介紹你可以看 Should I Use a Property or an Instance Variable?。

屬性定義

屬性可以存儲一個代碼塊。為了讓它存活到定義的塊的結束,必須使用 

copy

 (block 最早在棧裡面建立,使用 

copy

讓 block 拷貝到堆裡面去)

為了完成一個共有的 getter 和一個私有的 setter,你應該聲明公開的屬性為 

readonly

 并且在類擴充總重新定義通用的屬性為 

readwrite

 的。

相等性

一個完整的 isEqual 方法應該是這樣的:

<span style="font-size:14px;">- (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;
}</span>
           

一個對象執行個體的 

hash

 計算結果應該是确定的。當它被加入到一個容器對象(比如 

NSArray

NSSet

, 或者 

NSDictionary

)的時候這是很重要的,否則行為會無法預測(所有的容器對象使用對象的 hash 來查找或者實施特别的行為,如确定唯一性)這也就是說,應該用不可變的屬性來計算 hash 值,或者,最好保證對象是不可變的。

Categories

Protocols

Pragma

pragma-mark

#pragma-mark - NSObject

關于pragma

如果你知道你的代碼不會導緻記憶體洩露,你可以通過加入這些代碼忽略這些警告。

如:performSelector may cause a leak because its selector is unknown(執行 selector 可能導緻洩漏,因為這個 selector 是未知的)

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

[myObj performSelector:mySelector withObject:name];

#pragma clang diagnostic pop
           

注意我們是如何在相關代碼上下文中用 pragma 停用 -Warc-performSelector-leaks 檢查的。這確定我們沒有全局禁用。如果全局禁用,可能會導緻錯誤。

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

NSString *foo;
#pragma unused (foo)
           

明确編譯器警告和錯誤

對象間的通訊

Block

深入block

  • block 是在棧上建立的 
  • block 可以複制到堆上
  • block 有自己的私有的棧變量(以及指針)的常量複制
  • 可變的棧上的變量和指針必須用 __block 關鍵字聲明

self的循環引用

當使用代碼塊和異步分發的時候,要注意避免引用循環。 總是使用 

weak

 引用會導緻引用循環。 此外,把持有 block 的屬性設定為 nil (比如 

self.completionBlock = nil

) 是一個好的實踐。它會打破 block 捕獲的作用域帶來的引用循環。

例子:

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

多個語句的例子:

<pre name="code" class="objc">__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];
           

。。。。。。。

在一個 ARC 的環境中,如果嘗試用 

->

符号來表示,編譯器會警告一個錯誤:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個 __weak 指針的解引用不允許的,因為可能在競态條件裡面變成 null, 是以先把他定義成 strong 的屬性)
           

可以用下面的代碼展示

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

委托和資料源

委托模式是單向的,消息的發送方(委托方)需要知道接收方(委托),反過來就不是了。對象之間沒有多少耦合,因為發送方隻要知道它的委托實作了對應的 protocol。

本質上,委托模式隻需要委托提供一些回調方法,就是說委托實作了一系列空傳回值的方法。不幸的是 Apple 的 API 并沒有尊重這個原則,如UITableViewDelegate協定。

  • 委托模式:事件發生的時候,委托者需要通知委托
  • 資料源模式: 委托方需要從資料源對象拉取資料

多重委托

面向切面程式設計

繼續閱讀