提供指定構造器(Designated initializer
)
如果自己編寫框架或者檢視開源框架的一些初始化介面,或多或少都會發現,一般框架都會提供非常豐富便利的初始化介面,這些初始化介面的背後都會集中呼叫某個核心的初始化方法。
iOS的UIKit中的類,大多都會有一個核心的初始化方法或者稱為指定構造器,比如UIView
的- (instancetype)initWithFrame:(CGRect)frame
,UIViewController
的- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
這樣做的好處是,可以使類的初始化方法看起來簡單明瞭,減少程式碼量,方便維護初始化介面,當類發生一些改變的時候,只需要更改核心初始化方法
另外提供指定的構造器可以為屬性或者例項變數提供預設值,讓類在初始化的時候每個屬性都有合適的初始值,不至於是nil或者0,這樣可以避免一些錯誤,更加安全。比如如果提供了一個陣列屬性,程式碼寫了很多,最後除錯的時候不知道哪裡出錯,最後發現陣列沒有初始化或懶載入。
Object-C
《Effective Object-C 2.0》 第16條 就有建議提供指定初始化方法;相對於Swift的構造語法,Object-C初始化語法有點寬鬆,同時也帶來很多問題,拿《Effective Object-C 2.0》上的例子來說:
// EOCRectangle.h
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end
// EOCRectangle.m
- (instancetype)initWithWidth: (float)width andHeight: (float)height
{
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}
複製程式碼
1.由於Object-C會繼承父類的初始化方法,所有繼承NSObject
的類都有init
的這個方法,為了防止開發者沒有使用指定初始化方法還必須要重寫這個方法,UIKit
中大多數類也重寫了這個方法,拿上面的UIView
和UIViewController
來說,如果直接使用init
方法建立例項,底層還是會呼叫對應的指定初始化方法,拿上面的例子來說,如果要想使類變得完善,還需要處理繼承父類的init
方法;《Effective Object-C 2.0》中提供了兩種方式來處理,個人覺得這兩種方式都不能說優雅
- (instancetype)init
{
return [self initWithWidth: 5.0f andHeight:10.0f];
}
複製程式碼
如果不進行說明,還要檢視原始碼才知道預設值,這種方式如果指定初始化方法做出修改,類比較簡單還好控制,初始化方法比較複雜就會讓事情變得複雜
- (instancetype)init
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Must use initWithWidth: width andHeight: height"
userInfo:nil];
}
複製程式碼
這種方式就更加是無奈之舉,雖然安全性是有了保障,但是這是根本上違背了異常設計的初衷,我很難把呼叫這個方法想象成致命的錯誤
2.如果這時候再來一個繼承類的話,由於初始化方法的繼承又會使事情變得糟糕,詳情可以參考《Effective Object-C 2.0》第16條
總結:
1.儘可能的為類提供指定初始化方法,在初始化方法中為屬性提供合適的初始值
2.寫好指定初始化方法之後,處理繼承父類的初始化方法,如果父類的初始化方法不適用與子類,重寫父類的初始化方法並丟擲異常。
Swift
Swift構造器的規則在這方面就顯得非常現代化和安全,對於上面的問題,swift都有明確的規則進行限定(雖然swift的構造過程規則較多,理解之後使得構造邏輯非常清晰也變得簡單)
- 明確了指定構造器和便利構造器而且便利構造器還有關鍵字標明,甚至明確便利構造器的呼叫鏈的最後必須呼叫到同一類的指定構造器
- 類中的所有儲存型屬性必須在構造過程中設定初始值,Swift的兩段式構造過程還提供了構造過程的安全檢查,使程式碼更加安全
- swift中的子類不會預設繼承父類的構造器(滿足一定條件繼承父類的構造器,實際上還是為新引入的儲存屬性提供了預設的初始值,這樣也簡化了構造器的編寫)
對於上面的例子Swift就變得簡單的多
class Rectangle: NSObject {
let width: Float
let height: Float
init(width: Float,height: Float) {
self.width = width
self.height = height
}
}
class Square: Rectangle {
let dimension: Float
init(dimension: Float) {
self.dimension = dimension
super.init(width: dimension, height: dimension)
}
}
複製程式碼
對於Rectangle,它提供了指定構造器也就不會繼承NSObject的構造方法,你也只能使用指定構造器來例項化Rectangle
對於Square也一樣,不用像Object-C一樣來處理繼承的父類的構造方法