2018-08-12 21:19 更新

類名

類名應(yīng)加上 個大寫字母作為前綴(兩個字母的為 Apple 的類保留)。雖然這個規(guī)范看起來難看,但是這樣做是為了減少 objective-c 沒有命名空間所帶來的問題。

一些開發(fā)者在定義 Model 對象時并不遵循這個規(guī)范(對于 Core Data 對象,我們更應(yīng)該遵循這個規(guī)范)。我們建議在定義 Core Data 對象時嚴(yán)格遵循這個約定,因?yàn)槟阕詈罂赡馨涯愕?Managed Object Model 和其他(第三方庫)的 Managed Object Model 合并。

你可能注意到了,這本書里的類的前綴(其實(shí)不僅僅是類)是ZOC。

另一個類的命名規(guī)范:當(dāng)你創(chuàng)建一個子類的時候,你應(yīng)該把說明性的部分放在前綴和父類名的在中間。舉個例子:如果你有一個 ZOCNetworkClient 類,子類的名字會是ZOCTwitterNetworkClient (注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之間); 按照這個約定, 一個UIViewController 的子類會是 ZOCTimelineViewController.

Initializer 和 dealloc 初始化

推薦的代碼組織方式:將 dealloc 方法放在實(shí)現(xiàn)文件的最前面(直接在 @synthesize 以及 @dynamic 之后),init 應(yīng)該放在 dealloc 之后。如果有多個初始化方法, 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ā)生了什么呢?這是一個十分有趣的話題。

讓我們后退一步:我們曾經(jīng)寫了類似 [[NSObject alloc] init] 的表達(dá)式, allocinit 區(qū)別慢慢褪去 。一個 Objective-C 的特性叫 兩步創(chuàng)建 。 這意味著申請分配內(nèi)存和初始化是兩個分離的操作。

  • alloc表示對象分配內(nèi)存,這個過程涉及分配足夠的可用內(nèi)存來保存對象,寫入isa指針,初始化 retain 的計(jì)數(shù),并且初始化所有實(shí)例變量。
  • init 是表示初始化對象,這意味著把對象放到了一個可用的狀態(tài)。這通常是指把對象的實(shí)例變量賦給了可用的值。

alloc 方法會返回一個合法的沒有初始化的實(shí)例對象。每一個發(fā)送到實(shí)例的信息會被翻譯為名字是 selfalloc 返回的指針的參數(shù)返回的 objc_msgSend() 的調(diào)用。這樣之后 self 已經(jīng)可以執(zhí)行所有方法了。 為了完成兩步創(chuàng)建,第一個發(fā)送給新創(chuàng)建的實(shí)例的方法應(yīng)該是約定俗成的 init 方法。注意 NSObjectinit 實(shí)現(xiàn)中,僅僅是返回了 self。

關(guān)于 init 有一個另外的重要的約定:這個方法可以(并且應(yīng)該)在不能成功完成初始化的時候返回 nil;初始化可能因?yàn)楦鞣N原因失敗,比如一個輸入的格式錯誤,或者未能成功初始化一個需要的對象。 這樣我們就理解了為什么需要總是調(diào)用 self = [super init]。如果你的超類沒有成功初始化它自己,你必須假設(shè)你在一個矛盾的狀態(tài),并且在你的實(shí)現(xiàn)中不要處理你自己的初始化邏輯,同時返回 nil。如果你不是這樣做,你看你會得到一個不能用的對象,并且它的行為是不可預(yù)測的,最終可能會導(dǎo)致你的 app 發(fā)生 crash。

重新給 self 賦值同樣可以被 init 利用為在被調(diào)用的時候返回不同的實(shí)例。一個例子是 類簇 或者其他的返回相同的(不可變的)實(shí)例對象的 Cocoa 類。

Designated 和 Secondary Initializers

Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數(shù),secondary 初始化方法是一個或多個,并且提供一個或者更多的默認(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 初始化方法,另外的兩個是 secondary 初始化方法。因?yàn)樗鼈儍H僅是調(diào)用類實(shí)現(xiàn)的 designated 初始化方法

Designated Initializer

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

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

在類繼承中調(diào)用任何 designated 初始化方法都是合法的,而且應(yīng)該保證 所有的 designated initializer 在類繼承中是是從祖先(通常是 NSObject)到你的類向下調(diào)用的。 實(shí)際上這意味著第一個執(zhí)行的初始化代碼是最遠(yuǎn)的祖先,然后從頂向下的類繼承,所有類都有機(jī)會執(zhí)行他們特定的初始化代碼。這樣,你在你做你的特定的初始化工作前,所有你從超類繼承的東西是不可用的狀態(tài)。即使它的狀態(tài)不明確,所有 Apple 的框架的 Framework 是保證遵守這個約定的,而且你的類也應(yīng)該這樣做。

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

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

第一個方案是最簡單的:你不需要增加類的任何初始化邏輯,只需要依照父類的designated initializer。 當(dāng)你希望提供額外的初始化邏輯的時候你可以重載 designated initializer。你只需要重載你的直接的超類的 designated initializer 并且確認(rèn)你的實(shí)現(xiàn)調(diào)用了超類的方法。 你一個典型的例子是你創(chuàng)造UIViewController子類的時候重載 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 會是一個錯誤,這個情況下調(diào)用者會嘗試調(diào)用 initWithNib:bundle 初始化你的類,你的類實(shí)現(xiàn)不會被調(diào)用。著同樣違背了它應(yīng)該是合法調(diào)用任何 designated initializer 的規(guī)則。

在你希望提供你自己的初始化函數(shù)的時候,你應(yīng)該遵守這三個步驟來保證正確的性:

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

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

@implementation ZOCNewsViewController

- (id)initWithNews:(ZOCNews *)news
{
    // call to the immediate superclass's 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)用者決定用這個方法初始化你的類(這是完全合法的)。 initWithNews: 永遠(yuǎn)不會被調(diào)用,所以導(dǎo)致了不正確的初始化流程,你的類特定的初始化邏輯沒有被執(zhí)行。

即使可以推斷那個方法是 designate initializer它,但是最好清晰地明確(未來的你或者其他開發(fā)者在改代碼的時候會感謝你的)。你應(yīng)該考慮來用這兩個策略(不是互斥的):第一個是你在文檔中明確哪一個初始化方法是 designated 的,但是最好你可以用編譯器的指令 __attribute__((objc_designated_initializer)) 來標(biāo)記你的意圖。 用這個編譯指令的時候,編譯器回來幫你。如果你的新的 designate initializer 沒有調(diào)用你超類的 designated initializer,上編譯器會發(fā)出警告。 然而,當(dāng)沒有調(diào)用類的 designated initializer 的時候(并且依次提供必要的參數(shù)),并且調(diào)用其他父類中的 designated initialize 的時候,會變成一個不可用的狀態(tài)。參考之前的例子,當(dāng)實(shí)例化一個 ZOCNewsViewController 展示一個新聞而那條新聞沒有展示的話,就會毫無意義。這個情況下你應(yīng)該只需要讓其他的 designated initializer 失效,來強(qiáng)制調(diào)用一個非常特別的 designated initializer。通過使用另外一個編譯器指令 __attribute__((unavailable("Invoke the designated initializer"))) 來修飾一個方法,通過這個屬性,會讓你在試圖調(diào)用這個方法的時候產(chǎn)生一個編譯錯誤。

這是之前的例子相關(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

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

然而一個意外是一個對象是否遵守 NSCoding 協(xié)議,并且它通過方法 initWithCoder: 初始化。 我們應(yīng)該區(qū)別超類是否符合 NSCoding 的情況。 如果符合,如果你只是調(diào)用 [super initWithCoder:] 你會可能有一個共享的初始化代碼在 designated initializer 里面,一個好的方法是吧這些代碼放在私有方法里面(比如 p_commonInit )。 當(dāng)你的超類不符合NSCoding 協(xié)議的時候,推薦把 initWithCoder: 作為 secondary initializer 來對待,并且調(diào)用 self 的 designated initializer。 注意這是違反 Apple 的 Archives and Serializations Programming Guide 上面寫的:

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)該強(qiáng)制很多初始化操作在這樣的方法里面,并且你應(yīng)該一直假設(shè)這個方法不會得到調(diào)用。我們保證的是唯一被調(diào)用的方法是 designated initializer。 這意味著你的 designated initializer 總是應(yīng)該調(diào)用其他的 secondary initializer 或者你 self 的 designated initializer。有時候,因?yàn)殄e誤,可能打成了 super,這樣會導(dǎo)致不符合上面提及的初始化順序(在這個特別的例子里面,是跳過當(dāng)前類的初始化)

References 參考

instancetype

我們經(jīng)常忽略 Cocoa 充滿了約定,并且這些約定可以幫助編譯器變得更加聰明。無論編譯器是否遭遇 alloc 或者 init 方法,他會知道,即使返回類型都是 id ,這些方法總是返回接受到的類類型的實(shí)例。因此,它允許編譯器進(jìn)行類型檢查。(比如,檢查方法返回的類型是否合法)。Clang的這個好處來自于 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)于這個自動定義相關(guān)返回類型的約定請查看 Clang Language Extensions guide 的appropriate section

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

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

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

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

參考

初始化模式

類簇 (class cluster)

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

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

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

class cluster 的想法很簡單,你經(jīng)常有一個抽象類在初始化期間處理信息,經(jīng)常作為一個構(gòu)造器里面的參數(shù)或者環(huán)境中讀取,來完成特定的邏輯并且實(shí)例化子類。這個"public facing" 應(yīng)該知曉它的子類而且返回適合的私有子類。

這個模式非常有用,因?yàn)樗鼫p少了構(gòu)造器調(diào)用中的復(fù)雜性,只需要知道接口如何與對象通信,而不需要知道怎么實(shí)現(xiàn)。

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

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

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

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

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

通用的 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

之前的代碼的例子展示了如何創(chuàng)建一個類簇。首先,[self isMemberOfClass:ZOCKintsugiPhotoViewController.class] 來避免在子類中重載初始化方法,來避免無限的遞歸。當(dāng) [[ZOCKintsugiPhotoViewController alloc] initWithPhotos:photos] 得到調(diào)用的時候之前的檢查會變成 true 的,self = nil 是用來移除所有到 ZOCKintsugiPhotoViewController 實(shí)例的引用的,它會被釋放,按照這個邏輯來檢查哪個類應(yīng)該被初始化。 讓我們假設(shè)在 iPhone 上運(yùn)行了這個代碼, ZOCKintsugiPhotoViewController_iPhone 沒有重載initWithPhotos:,在這個情況下,當(dāng)執(zhí)行 self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos]; 的時候,ZOCKintsugiPhotoViewController 會被調(diào)用,并且當(dāng)?shù)谝淮螜z查的時候,這樣不會讓 ZOCKintsugiPhotoViewController 檢查會變成 false 調(diào)用return [super initWithNibName:nil bundle:nil]; ,這會讓 繼續(xù)初始化執(zhí)行正確的初始化之前的會話。

單例

如果可能,請盡量避免使用單例而是依賴注入。 然而,如果一定要用,請使用一個線程安全的模式來創(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í)行一次“,就像我們做的一樣。 這樣同時可以避免possible and sometimes prolific crashes.

經(jīng)典的可以接受的單例對象的例子是一個設(shè)備的 GPS 以及 動作傳感器。即使單例對象可以被子類化,這個情況可以十分有用。這個接口應(yīng)該證明給出的類是趨向于使用單例的。然而,經(jīng)常使用一個單獨(dú)的公開的 sharedInstance 類方法就夠了,并且不可寫的屬性也應(yīng)該被暴露。

把單例作為一個對象的容器來在代碼或者應(yīng)用層面上共享是糟糕和丑陋的,這是一個不好的設(shè)計(jì)。

屬性

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

例子 :

NSString *text;

不要這樣 :

NSString* text;
NSString * text;

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

使用屬性的自動同步 (synthesize) 而不是手動的 @synthesize 語句,除非你的屬性是 protocol 的一部分而不是一個完整的類。如果 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...) 這回定義更多相關(guān)的在ARC是錢,因?yàn)樗冀K是相關(guān)的。舉個例子,copy 每個時候你用 setter 并且傳送數(shù)據(jù)的時候,它會復(fù)制數(shù)據(jù)而不用額外的操作
  • KVO 通知(willChangeValueForKey, didChangeValueForKey) 會被自動執(zhí)行
  • 更容易debug:你可以設(shè)置一個斷點(diǎn)在屬性聲明上并且斷點(diǎn)會在每次 getter / setter 方法調(diào)用的時候執(zhí)行,或者你可以在自己的自定義 setter/getter 設(shè)置斷點(diǎn)。
  • 允許在一個單獨(dú)的地方為設(shè)置值添加額外的邏輯。

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

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

Init 和 Dealloc

有一個例外:你永遠(yuǎn)不能在 init (以及其他初始化函數(shù))里面用 getter 和 setter 方法,并且你直接訪問實(shí)例變量。事實(shí)上一個子類可以重載sette或者getter并且嘗試調(diào)用其他方法,訪問屬性的或者 ivar 的話,他們可能沒有完全初始化。記住一個對象是僅僅在 init 返回的時候,才會被認(rèn)為是初始化完成到一個狀態(tài)了。

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

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

點(diǎn)符號

當(dāng)使用 setter getter 方法的時候盡量使用點(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)存管理。 這樣做你的屬性更容易修改正確,并且更好閱讀。

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

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

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

@interface MyClass : NSObject
@property (nonatomic, readonly) NSObject *object
@end

@implementation MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object
@end

如果 BOOL 屬性的名字是描述性的,這個屬性可以省略 "is" ,但是特定要在 get 訪問器中指定名字,如:

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

文字和例子是引用 Cocoa Naming Guidelines.

為了避免 @synthesize 的使用,在實(shí)現(xiàn)文件中,Xcode已經(jīng)自動幫你添加了。

私有屬性

私有屬性應(yīng)該在類實(shí)現(xiàn)文件的類拓展(class extensions,沒有名字的 categories 中)中。有名字的 categories(如果 ZOCPrivate)不應(yīng)該使用,除非拓展另外的類。

例子:

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

可變對象

【疑問】

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

這個是用來確保包裝,并且在對象不知道的情況下避免改變值。

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

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

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

懶加載

當(dāng)實(shí)例化一個對象可能耗費(fèi)很多資源的,或者需要只配置一次并且有一些配置方法需要調(diào)用,而且你還不想弄亂這些方法。

在這個情況下,我們可以選擇使用重載屬性的 getter 方法來做 lazy 實(shí)例化。通常這種操作的模板像這樣:

- (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.SSSSS"];
  }
  return _dateFormatter;
}

即使在一些情況下這是有益的,但是我們?nèi)匀唤ㄗh你在決定這樣做之前經(jīng)過深思熟慮,事實(shí)上這樣是可以避免的。下面是使用 延遲實(shí)例化的爭議。

  • getter 方法不應(yīng)該有副作用。在使用 getter 方法的時候你不要想著它可能會創(chuàng)建一個對象或者導(dǎo)致副作用,事實(shí)上,如果調(diào)用 getter 方法的時候沒有涉及返回的對象,編譯器就會放出警告:getter 不應(yīng)該產(chǎn)生副作用
  • 你在第一次訪問的時候改變了初始化的消耗,產(chǎn)生了副作用,這回讓優(yōu)化性能變得困難(以及測試)
  • 這個初始化可能是不確定的:比如你期望屬性第一次被一個方法訪問,但是你改變了類的實(shí)現(xiàn),訪問器在你預(yù)期之前就得到了調(diào)用,這樣可以導(dǎo)致問題,特別是初始化邏輯可能依賴于類的其他不同狀態(tài)的時候??偟膩碚f最好明確依賴關(guān)系。
  • 這個行為不是 KVO 友好的。如果 getter 改變了引用,他應(yīng)該通過一個 KVO 通知來通知改變。當(dāng)訪問 getter 的時候收到一個改變的通知很奇怪。

方法

參數(shù)斷言

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

私有方法

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

相等性

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

這個約定是因?yàn)楫?dāng)被存儲在集合(如 NSDictionaryNSSet 在底層使用 hash 表數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu))的時候,如何查找這些對象。

@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 方法不能返回一個常量。這是一個典型的錯誤并且會導(dǎo)致嚴(yán)重的問題,因?yàn)槭褂昧诉@個值作為 hash 表的 key,會導(dǎo)致 hash 表 100%的碰撞

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

一個完整的 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;
}

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號