參考資料
iOS: 聊聊 Designated Initializer(指定初始化函數)
《編寫高質量iOS與OS X代碼的52個有效方法》中第16條:提供“全能初始化方法”
對象的創建
在 Objective-C 中,對象的創建分為兩步,分配內存和初始化成員變量。
NSObject *object = [[NSObject alloc] init];
首先調用類方法+ alloc
,其根據要創建的實例對象對應的類來分配足夠的內存空間。除了分配內存空間,其實+ alloc
方法還做了其他事情,包括將對象的引用計數記為1,將對象的isa
指針指向對應的運行時類對象,以及將對象的成員變量置為對應的0值(0、nil、NULL)。
+ alloc
方法返回的對象還是不可用的,在之后完成初始化方法的調用后,對象的創建工作才算完成。初始化方法會設置對象的成員變量為一個正確的合理的值,以及獲取一些其他額外的資源。
對象的初始化
Designated Initializer 指定初始化方法
所有對象都是要初始化的,而且很多情況下,對象在初始化時是需要接收額外的參數,這就可能會提供多個初始化方法。根據規范,通常選擇一個接收參數最多的初始化方法作為指定初始化方法,真正的數據分配和其他相關初始化操作在這個方法中完成。而其他的初始化方法則作為便捷初始化方法去調用這個指定初始化方法。這樣當實現改變時,只要修改指定初始化方法就可以了。便捷初始化方法接收的參數更少,它會在內部調用指定初始化方法時,直接設置未接收參數的默認值。便捷初始化方法也可以不直接調用指定初始化方法,它可以調用其他便捷初始化方法,但不管調用幾層,最終是要調用到指定初始化方法的,因為真正的實現操作是在指定初始化方法中完成的。所有初始化方法統一以- init
開始。
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
如上例代碼所示,- initWithTimeIntervalSinceReferenceDate
方法是一個指定初始化方法,而其他初始化方法最終是要調用它的。
子類實現指定初始化方法
當子類繼承父類后實現了新的指定初始化方法,此時如果調用父類中的指定初始化方法則無法調用到子類新實現的初始化邏輯,所以子類同時還要重寫父類的指定初始化方法,將其變為一個便捷初始化方法,最終去調用子類自己的指定初始化方法。而為了保證父類初始化邏輯的執行,在子類指定初始化方法中,首先要通過關鍵字super
調用父類的指定初始化方法。
@interface Rectangle : NSObject
@property (nonatomic) float width;
@property (nonatomic) float height;
@end
@implementation Rectangle
- (instancetype)init {
return [self initWithWidth:5 Height:5];
}
- (instancetype)initWithWidth:(float)width Height:(float)height {
if (self = [super init]) {
self.width = width;
self.height = height;
}
return self;
}
@end
@interface Square : Rectangle
@end
@implementation Square
- (instancetype)initWithWidth:(float)width Height:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
- (instancetype)initWithDimension:(float)dimension {
return [super initWithWidth:dimension Height:dimension];
}
@end
如上例代碼所示,Rectangle
類繼承自NSObject
類,它實現了新的指定初始化方法- initWithWidth:Height:
,而- init
方法是NSObject
類的指定初始化方法,如果不重寫則該初始化方法不會設置新增屬性的值。所以在Rectangle
類中的- init
方法通過self
關鍵字調用了指定初始化方法。而在指定初始化方法中,通過[super init]
調用了父類的初始化邏輯。同理,Square
類又繼承自Rectangle
類,它新實現了指定初始化方法- initWithDimension:
, 并調用了父類的指定初始化方法,所以要重寫方法- initWithWidth:Height:
,而此時因為- init
方法已重寫為便捷方法,會最終調用到新的指定初始化方法,所以不需要重寫了。
在子類實現新的指定初始化話方法時,除了將父類的指定初始化方法重寫為便捷方法外,也可以在重寫實現中拋出異常,即告訴外界在子類中是不提供這種初始化方式的。
如果子類不需要實現自己的指定初始化方法,或者子類的指定初始化方法就是重寫父類的指定初始化方法,則其他的子類便捷初始化方法,就調用子類中這個與父類指定初始化方法的同名方法即可。
- initWithCoder:
框架中的很多類實現了<NSCoding>
協議(如:UIViewController
),這個協議定義了初始化方法- initWithCoder:
,一般這個方法里的初始化邏輯與其他的指定初始化方法中是不同的,如UIViewController
通過該方法解碼 XML 格式的 NIB 文件。所以子類中可以有不止一個指定初始化方法,- initWithCoder:
也是一個指定初始化方法。- initWithCoder:
中的相關實現規則是,如果父類也實現了<NSCoding>
協議,首先要調用父類的- initWithCoder:
方法,如果父類沒有實現,則調用父類的指定初始化方法。
NS_DESIGNATED_INITIALIZER
當在接口中指定初始化方法的后面加上該宏,編譯器就會檢查我們實現的初始化調用鏈是否符合規則,并提示相應的警告。另外NS_DESIGNATED_INITIALIZER
也起到了標明指定初始化方法的注釋作用。
- (instancetype)init NS_DESIGNATED_INITIALIZER;
總結
指定初始化方法的機制保證了對象會依次執行從父類到子類的所有初始化邏輯,實現的規則為:
- 便捷初始化方法只能調用本類中的其他初始化方法,并最終調用到指定初始化方法。
- 子類的指定初始化方法要調用父類的指定初始化方法,以保證父類的初始化邏輯可以執行。
- 當子類實現了自己的指定初始化方法后,父類的指定初始化方法要重寫為便捷初始化方法,以保證所有初始化方法都能調用到子類的初始化邏輯。