所有物件在被使用前都要先進行初始化。對於最簡單的情況,我們只需要使用 init
方法進行初始化就可以了。然而,大多數情況下,物件的初始化都需要我們提供額外的資訊,並且有時建立例項的方法不止一種,這時我們就需要提供多個構造方法。
對於需要多個構造方法的情況,我們需要確保有一個指定構造方法(designated initializer)
,指定構造方法用於為類提供必要的資訊使其能完成初始化工作,所有其它的構造方法都要呼叫指定構造方法。這些其它的構造方法,我們通常稱為“便利構造方法”,它們只能直接或者間接地呼叫指定構造方法,而不能直接物件進行初始化。
一個例項
假設我們有一個 Human 類,這個類有一個初始化方法:
1 2 3 4 5 |
@interface Human: NSObject @property (strong, nonatomic) NSString *name; - (instancetype)initWithName:(NSString *)name; @end |
實現:
1 2 3 4 5 6 7 8 9 10 |
@implement Human - (instancetype)initWithName:(NSString *)name { if (self = [super init]) { _name = name; } return self; } @end |
在這個類裡面,initWithName:
就是它的指定構造方法,正常情況下,我們都應該呼叫它來對 Human 物件進行初始化。
但是如果有人偏偏要用 init
來進行初始化呢?這種情況下,name 會被自動初始化成 nil,但這可能不是我們想要的結果,所以我們可以對 init
進行覆寫,然後在覆寫的方法裡面呼叫自身的指定構造方法:
1 2 3 |
- (instancetype)init { return [self initWithName:@"Nerd"]; } |
現在建立一個 Programmer 類並繼承自 Human:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@interface Programmer: Human @property (readonly) NSArray *skills; - (instancetype)initWithName:(NSString *)name skill:(NSString *)skill; @end @implement Programmer { NSArray *_skills; } - (instancetype)initWithName:(NSString *)name skill:(NSString *)skill { if (self = [super initWithName:name]) { _skills = [NSMutableArray array]; [_skills addObject:skill]; } return self; } @end |
在子類的構造方法裡面,只需要處理子類新引用的屬性,對於父類的初始化,只需要呼叫父類的指定構造方法就可以了。所以 在子類的指定構造方法裡面,一定要呼叫父類的指定構造方法,這樣才能保證所有必要的屬性都被正確地初始化。
跟剛剛一樣,這裡也存在一個問題,我們有可能不經意間使用下面的程式碼對 Programmer 物件進行初始化:
1 |
Programmer *p = [Programmer alloc] initWithName:@"Dennis Ritchie"]; |
使用這種方式初始化出來的 Programmer 物件將對它掌握的技能(skill)一無所知。
同樣的,我們可以覆寫父類的指定構造方法來解決這個問題:
1 2 3 |
- (instancetype)initWithName:(NSString *)name { return [self initWithName:name skill:@"Objective-C"]; } |
你可能會想說,那我們是不是還需要實現 init
方法,答案是“不需要”。
當對 Programmer 物件呼叫 init
構造方法的時候,因為它沒有實現這個方法,所以會變成呼叫其父類,即 Human 的 init
方法。而 Human 的 init
方法裡面呼叫的是 [self initWithName:]
,這時的 self 是一個 Programmer 物件,所以它會呼叫 Programmer 的 initWithName:
方法,這個方法最終會呼叫到 Programmer 的指定構造方法,也即 initWithName:skill:
。
也就是說,只要我們在繼承鏈當中的每個類都實現了指定構造方法,並保證其它的構造方法都呼叫到了指定構造方法,就可以保證整個初始化過程的正確性。
規則
以上的內容可以總結成幾條規則:
- 當一個類有多個構造方法的時候,要保證只有一個能真正對物件進行初始化,即指定構造方法。而其它的便利構造方法都要直接或者間接地呼叫指定構造方法。
- 指定構造方法需要先呼叫父類的指定構造方法,然後再對自身類的屬性進行初始化。
- 如果子類的指定構造方法與父類不同,則該子類需要覆寫父類的指定構造方法,並在該實現裡面呼叫自身的指定構造方法。
- 如果一個類有多個構造方法,需要在標頭檔案中指定哪個是指定構造方法。
NS_DESIGNATED_INITIALIZER
對於上面提到的規則中的最後一條,以前都是在標頭檔案中使用註釋來說明哪個構造方法是指定構造方法。這種方法的缺點在於註釋有可能過時,有時更新程式碼後卻沒有更新註釋,這都有可能給後繼的開發造成困擾。
自從 Xcode 6 開始,有了一個新的巨集 — NS_DESIGNATED_INITIALIZER
。只要在標頭檔案中使用該巨集表明哪個構造方法是指定構造方法,之後如果我們在實現構造方法的時候違反了上面的規則,編譯器就會給出警告。
1 2 3 4 5 6 7 8 |
// UIView - (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER; // NSDate - (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER; // NSSet - (instancetype)initWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER; |
結論
指定構造方法在 Objective-C 當中已經存在很久了,但是它真正開始迅速受到關注還利益於 Swift。
在 Swift 當中,所有的 init 方法都是指定構造方法,其它的便利構造方法都需要在 init 方法前加上 convince 關鍵字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class BankAccount { var name: String var balance: Int convenience init(name: String) { self.init(name: name, balance: 0) } convenience init(balance: Int) { self.init(name: NSLocalizedString("Anonymous", nil), balance: balance) } init(name: String, balance: Int) { self.name = name self.balance = balance } } |
由於 Swift 中對初始化過程的檢查更加嚴格了,一個類中的所有屬性都必須被初始化,只要違反了前面小節中提到的規則中的任意一條,都會導致無法通過編譯。
當我們開始用 NS_DESIGNATED_INITIALIZER 對 Objective-C 的指定構造方法進行標記,或者在 Swift 當中使用 convince 關鍵字的時候,都能更好地在程式碼中表達我們的意圖,並且讓編譯器來幫助我們減少一些不必要的 BUG。