01-禪與 Objective-C 編程藝術之條件語句與命名
類
類名
類名應該以三個大寫字母作為前綴(雙字母前綴為 Apple 的類預留)。盡管這個規范看起來有些古怪,但是這樣做可以減少 Objective-C 沒有命名空間所帶來的問題。
一些開發者在定義模型對象時并不遵循這個規范(對于 Core Data 對象,我們更應該遵循這個規范)。我們建議在定義 Core Data 對象時嚴格遵循這個約定,因為最終你可能需要把你的 Managed Object Model(托管對象模型)與其他(第三方庫)的 MOMs(Managed Object Model)合并。
你可能注意到了,這本書里類的前綴(不僅僅是類,也包括公開的常量、Protocol 等的前綴)是ZOC。
另一個好的類的命名規范:當你創建一個子類的時候,你應該把說明性的部分放在前綴和父類名的在中間。
舉個例子:如果你有一個 ZOCNetworkClient
類,子類的名字會是ZOCTwitterNetworkClient
(注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之間); 按照這個約定, 一個UIViewController
的子類會是 ZOCTimelineViewController
.
Initializer 和 dealloc
推薦的代碼組織方式是將 dealloc 方法放在實現文件的最前面(直接在 @synthesize 以及 @dynamic 之后),init 應該跟在 dealloc 方法后面。
如果有多個初始化方法, 指定初始化方法 (designated initializer) 應該放在最前面,間接初始化方法 (secondary initializer) 跟在后面,這樣更有邏輯性。如今有了 ARC,dealloc 方法幾乎不需要實現,不過把 init 和 dealloc 放在一起可以從視覺上強調它們是一對的。通常,在 init 方法中做的事情需要在 dealloc 方法中撤銷。
init 方法應該是這樣的結構:
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
為什么設置 self
為 [super init]
的返回值,以及中間發生了什么呢?這是一個十分有趣的話題。
我們退一步講:我們常常寫[[NSObject alloc] init]
這樣的代碼,從而淡化了alloc
和 init
的區別。Objective-C 的這個特性叫做 兩步創建 。
這意味著申請分配內存和初始化被分離成兩步,alloc
和init
。
-
alloc
負責創建對象,這個過程包括分配足夠的內存來保存對象,寫入isa
指針,初始化引用計數,以及重置所有實例變量。 -
init
負責初始化對象,這意味著使對象處于可用狀態。這通常意味著為對象的實例變量賦予合理有用的值。
alloc
方法將返回一個有效的未初始化的對象實例。每一個對這個實例發送的消息會被轉換成一次objc_msgSend()
函數的調用,形參 self
的實參是 alloc
返回的指針;這樣 self
在所有方法的作用域內都能夠被訪問。
按照慣例,為了完成兩步創建,新創建的實例第一個被調用的方法將是 init 方法。注意,NSObject 在實現 init 時,只是簡單的返回了 self。
關于 init
的約定還有一個重要部分:這個方法可以(并且應該)通過返回 nil 來告訴調用者,初始化失敗了;初始化可能會因為各種原因失敗,比如一個輸入的格式錯誤了,或者另一個需要的對象初始化失敗了。 這樣我們就能理解為什么總是需要調用 self = [super init]
。如果你的父類說初始化自己的時候失敗了,那么你必須假定你正處于一個不穩定的狀態,因此在你的實現里不要繼續你自己的初始化并且也返回 nil
。如果不這樣做,你可能會操作一個不可用的對象,它的行為是不可預測的,最終可能會導致你的程序崩潰。
init
方法在被調用的時候可以通過重新給 self
重新賦值來返回另一個實例,而非調用的那個實例。例如類簇,還有一些 Cocoa 類為相等的(不可變的)對象返回同一個實例。
Designated 和 Secondary 初始化方法
Objective-C 有指定初始化方法(designated initializer
)和間接(secondary initializer
)初始化方法的觀念。 designated
初始化方法是提供所有的參數,secondary
初始化方法是一個或多個,并且提供一個或者更多的默認參數來調用 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
初始化方法。因為它們僅僅是調用類實現的 designated
初始化方法
Designated Initializer
一個類應該有且只有一個 designated
初始化方法,其他的初始化方法應該調用這個 designated
的初始化方法(雖然這個情況有一個例外)
這個分歧沒有要求那個初始化函數需要被調用。
在類繼承中調用任何 designated
初始化方法都是合法的,而且應該保證 所有的 designated initializer
在類繼承中是從祖先(通常是 NSObject
)到你的類向下調用的。
實際上這意味著第一個執行的初始化代碼是最遠的祖先,然后從頂向下的類繼承,所有類都有機會執行他們特定初始化代碼。這樣,你在做特定初始化工作前,所有從超類繼承的東西都是不可用的狀態。 雖然這沒有明確的規定,但是所有 Apple 的框架都保證遵守這個約定,你的類也應該這樣做。
當定義一個新類的時候有三個不同的方式:
- 不需要重載任何初始化函數
- 重載 designated initializer
- 定義一個新的 designated initializer
第一個方案是最簡單的:你不需要增加類的任何初始化邏輯,只需要依照父類的designated initializer
。
當你希望提供額外的初始化邏輯的時候,你可以重載designated initializer
。你只需要重載直接超類的 designated initializer
并且確認你的實現調用了超類的方法。
一個典型的例子是你創造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
會是一個錯誤,這個情況下調用者會嘗試調用 initWithNib:bundle
初始化你的類,你的類實現不會被調用。這同樣違背了它應該是合法調用任何 designated initializer 的規則。
在你希望提供你自己的初始化函數的時候,你應該遵守這三個步驟來保證獲得正確的行為:
- 定義你的 designated initializer,確保調用了直接超類的 designated initializer。
- 重載直接超類的 designated initializer。調用你的新的 designated initializer。
- 為新的 designated initializer 寫文檔。
很多開發者忽略了后兩步,這不僅僅是一個粗心的問題,而且這樣違反了框架的規則,可能導致不確定的行為和bug。 讓我們看看正確的實現的例子:
@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
// call to the immediate superclass's designated initializer (調用直接超類的 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:
,而且調用者決定用這個方法初始化你的類(這是完全合法的)。initWithNews:
永遠不會被調用,所以導致了不正確的初始化流程,你的類的特定初始化邏輯沒有被執行。
即使可以推斷那個方法是 designated initializer,也最好清晰地明確它(未來的你或者其他開發者在改代碼的時候會感謝你的)。
你應該考慮來用這兩個策略(不是互斥的):第一個是你在文檔中明確哪一個初始化方法是 designated 的,你可以用編譯器的指令 __attribute__((objc_designated_initializer))
來標記你的意圖。
用這個編譯指令的時候,編譯器會來幫你。如果你的新的 designated initializer 沒有調用超類的 designated initializer,那么編譯器會發出警告。
然而,當沒有調用類的 designated initializer 的時候(并且依次提供必要的參數),并且調用其他父類中的 designated initialize 的時候,會變成一個不可用的狀態。參考之前的例子,當實例化一個 ZOCNewsViewController
展示一個新聞而那條新聞沒有展示的話,就會毫無意義。這個情況下你應該只需要讓其他的 designated initializer 失效,來強制調用一個非常特別的 designated initializer。通過使用另外一個編譯器指令 __attribute__((unavailable("Invoke the designated initializer")))
來修飾一個方法,通過這個屬性,會讓你在試圖調用這個方法的時候產生一個編譯錯誤。
這是之前的例子相關的實現的頭文件(這里使用宏來讓代碼沒有那么啰嗦)
@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
上述的一個推論是:你應該永遠不從 designated initializer 里面調用一個 secondary initializer (如果secondary initializer 遵守約定,它會調用 designated initializer)。如果這樣,調用很可能會調用一個子類重寫的 init 方法并且陷入無限遞歸之中。
不過一個例外是一個對象是否遵守 NSCoding 協議,并且它通過方法 initWithCoder:
初始化。 我們應該看超類是否符合NSCoding
協議來區別對待。 符合的時候,如果你只是調用[super initWithCoder:]
,你可能需要在 designated initializer 里面寫一些通用的初始化代碼,處理這種情況的一個好方法是把這些代碼放在私有方法里面(比如 p_commonInit )。 當你的超類不符合 NSCoding 協議的時候,推薦把initWithCoder:
作為 secondary initializer 來對待,并且調用 self 的 designated initializer。 注意這違反了 Apple 寫在 Archives and Serializations Programming Guide
上面的規定:
the object should first invoke its superclass's designated initializer to initialize inherited state(對象總是應該首先調用超類的 designated initializer 來初始化繼承的狀態)
如果你的類不是 NSObject
的直接子類,這樣做的話,會導致不可預測的行為。
Secondary Initializer
正如之前的描述,secondary initializer 是一種提供默認值、行為到 designated initializer的方法。也就是說,在這樣的方法里面你不應該有初始化實例變量的操作,并且你應該一直假設這個方法不會得到調用。我們保證的是唯一被調用的方法是 designated initializer。 這意味著你的 secondary initializer 總是應該調用 Designated initializer 或者你自定義(上面的第三種情況:自定義Designated initializer)的 self的 designated initializer。有時候,因為錯誤,可能打成了 super,這樣會導致不符合上面提及的初始化順序(在這個特別的例子里面,是跳過當前類的初始化)
** 參考 **
https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/ObjectCreation.html
https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Initialization/Initialization.html
https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/MultipleInitializers.html
https://blog.twitter.com/2014/how-to-objective-c-initializer-patterns
instancetype
我們經常忽略 Cocoa 充滿了約定,并且這些約定可以幫助編譯器變得更加聰明。無論編譯器是否遭遇 alloc 或者 init 方法,他會知道,即使返回類型都是 id ,這些方法總是返回接受到的類類型的實例。因此,它允許編譯器進行類型檢查。(比如,檢查方法返回的類型是否合法)。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 (發送到 alloc 或者 init 方法的消息會有同樣的靜態類型檢查是否為接受類的實例。)
更多的關于這個自動定義相關返回類型的約定請查看 Clang Language Extensions guide 的appropriate section
一個相關的返回類型可以明確地規定用 instancetype 關鍵字作為返回類型,并且它可以在一些工廠方法或者構造器方法的場景下很有用。它可以提示編譯器正確地檢查類型,并且更加重要的是,這同時適用于它的子類。
@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name;
@end
雖然如此,根據 clang 的定義,id 可以被編譯器提升到 instancetype 。在 alloc 或者 init 中,我們強烈建議對所有返回類的實例的類方法和實例方法使用 instancetype 類型。
在你的 API 中要構成習慣以及保持始終如一的,此外,通過對你代碼的小調整你可以提高可讀性:在簡單的瀏覽的時候你可以區分哪些方法是返回你類的實例的。你以后會感謝這些注意過的小細節的。
參考
http://tewha.net/2013/02/why-you-should-use-instancetype-instead-of-id/
http://tewha.net/2013/01/when-is-id-promoted-to-instancetype/
http://clang.llvm.org/docs/LanguageExtensions.html#related-result-types
http://nshipster.com/instancetype/
初始化模式
類簇 (class cluster)
類簇在Apple的文檔中這樣描述:
an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個在共有的抽象超類下設置一組私有子類的架構)
如果這個描述聽起來很熟悉,說明你的直覺是對的。 Class cluster 是 Apple 對抽象工廠設計模式的稱呼。
class cluster 的想法很簡單: 使用信息進行(類的)初始化處理期間,會使用一個抽象類(通常作為初始化方法的參數或者判定環境的可用性參數)來完成特定的邏輯或者實例化一個具體的子類。而這個"Public Facing(面向公眾的)"類,必須非常清楚他的私有子類,以便在面對具體任務的時候有能力返回一個恰當的私有子類實例。對調用者來說只需知道對象的各種API的作用即可。這個模式隱藏了他背后復雜的初始化邏輯,調用者也不需要關心背后的實現。
Class clusters 在 Apple 的Framework 中廣泛使用:一些明顯的例子比如NSNumber
可以返回不同類型給你的子類,取決于 數字類型如何提供 (Integer, Float, etc...) 或者NSArray
返回不同的最優存儲策略的子類。
這個模式的精妙的地方在于,調用者可以完全不管子類,事實上,這可以用在設計一個庫,可以用來交換實際的返回的類,而不用去管相關的細節,因為它們都遵從抽象超類的方法。
我們的經驗是使用類簇可以幫助移除很多條件語句。
一個經典的例子是如果你有為 iPad 和 iPhone 寫的一樣的 UIViewController 子類,但是在不同的設備上有不同的行為。
比較基礎的實現是用條件語句檢查設備,然后執行不同的邏輯。雖然剛開始可能不錯,但是隨著代碼的增長,運行邏輯也會趨于復雜。 一個更好的實現的設計是創建一個抽象而且寬泛的 view controller 來包含所有的共享邏輯,并且對于不同設備有兩個特別的子例。
通用的 view controller 會檢查當前設備并且返回適當的子類。
@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
這個子例程展示了如何創建一個類簇。
使用[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]
防止子類中重載初始化方法,避免無限遞歸。當[[ZOCKintsugiPhotoViewController alloc] initWithPhotos:photos]
被調用時,上面條件表達式的結果將會是True。
self = nil
的目的是移除ZOCKintsugiPhotoViewController
實例上的所有引用,實例(抽象類的實例)本身將會解除分配( 當然ARC也好MRC也好dealloc都會發生在Main Runloop這一次的結束時)。
接下來的邏輯就是判斷哪一個私有子類將被初始化。我們假設在iPhone上運行這段代碼并且ZOCKintsugiPhotoViewController_iPhone
沒有重載initWithPhotos:
方法。這種情況下,當執行self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
,ZOCKintsugiPhotoViewController
將會被調用,第一次檢查將會在這里發生,鑒于ZOCKintsugiPhotoViewController_iPhone
不完全是ZOCKintsugiPhotoViewController
,表達式[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]
將會是False,于是就會調用[super initWithNibName:nil bundle:nil]
,于是就會進入ZOCKintsugiPhotoViewController
的初始化過程,這時候因為調用者就是ZOCKintsugiPhotoViewController
本身,這一次的檢查必定為True,接下來就會進行正確的初始化過程。(NOTE:這里必須是完全遵循Designated initializer 以及Secondary initializer的設計規范的前提下才會其效果的!不明白這個規范的可以后退一步熟悉這種規范在回頭來看這個說明)
NOTE: 這里的意思是,代碼是在iPhone上調試的,程序員使用了
self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
來初始化某個view controller
的對象,當代碼運行在iPad上時,這個初始化過程也是正確的,因為無論程序員的代碼中使用self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
來初始化viewController
(iPhone上編寫運行在iPad上),還是使用self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
來初始化viewController
(iPad上編寫,運行在iPhone上),都會因為ZOCKintsugiPhotoViewController
的initWithPhotos:
方法的存在而變得通用起來。
單例
如果可能,請盡量避免使用單例而是依賴注入。 然而,如果一定要用,請使用一個線程安全的模式來創建共享的實例。對于 GCD,用 dispatch_once()
函數就可以咯。
+ (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()
的優點是,它更快,而且語法上更干凈,因為dispatch_once()
的意思就是 “把一些東西執行一次”,就像我們做的一樣。 這樣同時可以避免 possible and sometimes prolific crashes.
經典的單例對象是:一個設備的GPS以及它的加速度傳感器(也稱動作感應器)。 雖然單例對象可以子類化,但這種方式能夠有用的情況非常少見。 必須有證據表明,給定類的接口趨向于作為單例來使用。 所以,單例通常公開一個sharedInstance的類方法就已經足夠了,沒有任何的可寫屬性需要被暴露出來。
嘗試著把單例作為一個對象的容器,在代碼或者應用層面上共享,是一個糟糕和丑陋的設計。
NOTE:單例模式應該運用于類及類的接口趨向于作為單例來使用的情況 (譯者注)
屬性
屬性應該盡可能描述性地命名,避免縮寫,并且是小寫字母開頭的駝峰命名。我們的工具可以很方便地幫我們自動補全所有東西(嗯。。幾乎所有的,Xcode 的Derived Data 會索引這些命名)。所以沒理由少打幾個字符了,并且最好盡可能在你源碼里表達更多東西。
例子 :
NSString *text;
不要這樣 :
NSString* text;
NSString * text;
(注意:這個習慣和常量不同,這是主要從常用和可讀性考慮。 C++ 的開發者偏好從變量名中分離類型,作為類型它應該是NSString*
(對于從堆中分配的對象,對于C++是能從棧上分配的)格式。)
使用屬性的自動同步 (synthesize
) 而不是手動的@synthesize
語句,除非你的屬性是 protocol
的一部分而不是一個完整的類。如果 Xcode 可以自動同步這些變量,就讓它來做吧。否則只會讓你拋開 Xcode 的優點,維護更冗長的代碼。
你應該總是使用 setter
和 getter
方法訪問屬性,除了 init
和 dealloc
方法。通常,使用屬性讓你增加了在當前作用域之外的代碼塊的可能所以可能帶來更多副作用。
你總應該用 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?。
Init 和 Dealloc
有一個例外:永遠不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你應當直接訪問實例變量。這樣做是為了防止有子類時,出現這樣的情況:它的子類最終重載了其 setter 或者 getter 方法,因此導致該子類去調用其他的方法、訪問那些處于不穩定狀態,或者稱為沒有初始化完成的屬性或者 ivar 。記住一個對象僅僅在 init 返回的時候,才會被認為是達到了初始化完成的狀態。
同樣在 dealloc 方法中(在 dealloc 方法中,一個對象可以在一個 不確定的狀態中)這是同樣需要被注意的。
Advanced Memory Management Programming Guide under the self-explanatory section "Don't Use Accessor Methods in Initializer Methods and dealloc";
Migrating to Modern Objective-C at WWDC 2012 at slide 27;
in a pull request form Dave DeLong's.
此外,在 init 中使用 setter 不會很好執行 UIAppearence 代理(參見 UIAppearance for Custom Views 看更多相關信息)。
點符號
當使用 setter getter 方法的時候盡量使用點符號。應該總是用點符號來訪問以及設置屬性。
例子:
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
不要這樣:
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
使用點符號會讓表達更加清晰并且幫助區分屬性訪問和方法調用
屬性定義
推薦按照下面的格式來定義屬性
@property (nonatomic, readwrite, copy) NSString *name;
屬性的參數應該按照下面的順序排列: 原子性,讀寫 和 內存管理。 這樣做你的屬性更容易修改正確,并且更好閱讀。(譯者注:習慣上修改某個屬性的修飾符時,一般從屬性名從右向左搜索需要修動的修飾符。最可能從最右邊開始修改這些屬性的修飾符,根據經驗這些修飾符被修改的可能性從高到底應為:內存管理 > 讀寫權限 >原子操作)
你必須使用 nonatomic,除非特別需要的情況。在iOS中,atomic帶來的鎖特別影響性能。
屬性可以存儲一個代碼塊。為了讓它存活到定義的塊的結束,必須使用 copy (block 最早在棧里面創建,使用 copy讓 block 拷貝到堆里面去)
為了完成一個共有的 getter 和一個私有的 setter,你應該聲明公開的屬性為 readonly 并且在類擴展中重新定義通用的屬性為 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不應該帶is
前綴,但它對應的 getter 訪問器應該帶上這個前綴,如:
@property (assign, getter=isEditable) BOOL editable;
文字和例子引用自 Cocoa Naming Guidelines。
在實現文件中應避免使用@synthesize,因為Xcode已經自動為你添加了。
私有屬性
私有屬性應該定義在類的實現文件的類的擴展 (匿名的 category) 中。不允許在有名字的 category(如 ZOCPrivate)中定義私有屬性,除非你擴展其他類。
例子:
@interface ZOCViewController ()
@property (nonatomic, strong) UIView *bannerView;
@end
可變對象
任何可以用一個可變的對象設置的((比如 NSString
,NSArray
,NSURLRequest
))屬性的內存管理類型必須是 copy
的。
這是為了確保防止在不明確的情況下修改被封裝好的對象的值(譯者注:比如執行 array(定義為 copy 的 NSArray 實例) = mutableArray,copy 屬性會讓 array 的 setter 方法為 array = [mutableArray copy], [mutableArray copy] 返回的是不可變的 NSArray 實例,就保證了正確性。用其他屬性修飾符修飾,容易在直接賦值的時候,array 指向的是 NSMuatbleArray 的實例,在之后可以隨意改變它的值,就容易出錯)。
你應該同時避免暴露在公開的接口中可變的對象,因為這允許你的類的使用者改變類自己的內部表示并且破壞類的封裝。你可以提供可以只讀的屬性來返回你對象的不可變的副本。
/* .h */
@property (nonatomic, readonly) NSArray *elements
/* .m */
- (NSArray *)elements {
return [self.mutableElements copy];
}
懶加載(Lazy Loading)
當實例化一個對象需要耗費很多資源,或者配置一次就要調用很多配置相關的方法而你又不想弄亂這些方法時,我們需要重寫 getter 方法以延遲實例化,而不是在 init 方法里給對象分配內存。通常這種操作使用下面這樣的模板:
- (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;
}
即使這樣做在某些情況下很不錯,但是在實際這樣做之前應當深思熟慮。事實上,這樣的做法是可以避免的。下面是使用延遲實例化的爭議。
- getter 方法應該避免副作用。看到 getter 方法的時候,你不會想到會因此創建一個對象或導致副作用,實際上如果調用 getter 方法而不使用其返回值編譯器會報警告 “Getter 不應該僅因它產生的副作用而被調用”。
副作用指當調用函數時,除了返回函數值之外,還對主調用函數產生附加的影響。例如修改全局變量(函數外的變量)或修改參數。函數副作用會給程序設計帶來不必要的麻煩,給程序帶來十分難以查找的錯誤,并且降低程序的可讀性。(譯者注)
- 你在第一次訪問的時候改變了初始化的消耗,產生了副作用,這會讓優化性能變得困難(以及測試)
- 這個初始化可能是不確定的:比如你期望屬性第一次被一個方法訪問,但是你改變了類的實現,訪問器在你預期之前就得到了調用,這樣可以導致問題,特別是初始化邏輯可能依賴于類的其他不同狀態的時候。總的來說最好明確依賴關系。
- 這個行為不是 KVO 友好的。如果 getter 改變了引用,他應該通過一個 KVO 通知來通知改變。當訪問 getter 的時候收到一個改變的通知很奇怪。
方法
參數斷言
你的方法可能要求一些參數來滿足特定的條件(比如不能為nil),在這種情況下最好使用 NSParameterAssert()
來斷言條件是否成立或是拋出一個異常。
私有方法
永遠不要在你的私有方法前加上 _
前綴。這個前綴是 Apple 保留的。不要冒重載蘋果的私有方法的險。
相等性
當你要實現相等性的時候記住這個約定:你需要同時實現isEqual
和 hash
方法。如果兩個對象是被isEqual
認為相等的,它們的 hash
方法需要返回一樣的值。但是如果hash
返回一樣的值,并不能確保他們相等。
這個約定當對象被存儲在集合中(如 NSDictionary
和NSSet
在底層使用 hash
表數據的數據結構)的時候,用來查找這些對象的。
@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 方法不能返回一個常量。這是一個典型的錯誤并且會導致嚴重的問題,因為實際上hash方法的返回值會作為對象在 hash 散列表中的 key,這會導致 hash 表 100% 的碰撞。
你總是應該用isEqualTo<#class-name-without-prefix#>:
這樣的格式實現一個相等性檢查方法。如果你這樣做,會優先調用這個方法來避免上面的類型檢查。
一個完整的 isEqual
方法應該是這樣的:
- (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;
}
譯者注: 一般而言我們會直接調用自定義的isEqualTo__ClassName__:方法,對類的實例判等。
像相等性的開篇已經提到的那樣,這里應該復寫isEqual:方法,因為NSObject的isEqual:方法顯然不會考慮我們自定義類的類型判斷及屬性的相等性。當我們自定義的類的對象處在無序集合中被查找時,會自動調用isEqual:。同樣的該類的hash方法,也會在集合查找對象的時候被使用,我們也可以通過復寫hash方法以達到用自己的標準來判定對象是否hash等同。
我們實現的hash方法應該建立在系統提供的各種對象的hash方法之上(像開篇的例程那樣)。不推薦自己去實現某種hash算法來替代系統提供的hash算法,這一般而言會大大影響性能或者準確性,系統提供的hash算法已經經過無數次修繕,足以滿足你的要求。
一個對象實例的 hash 計算結果應該是確定的。當它被加入到一個容器對象(比如 NSArray, NSSet, 或者 NSDictionary)的時候這是很重要的,否則行為會無法預測(所有的容器對象使用對象的 hash 來查找或者實施特別的行為,如確定唯一性)這也就是說,應該用不可變的屬性來計算 hash 值,或者,最好保證對象是不可變的。