原文地址:http://huizhao.win/2016/11/13/swift-init/
從 Objective-C 轉到 Swift 后,可能首先就會發覺 Swift 的初始化方法變了,曾經 Objective-C 里面隨意信手拈來的初始化代碼可能不好使了,一起來學習一下吧。
初始化方法調用順序
分別創建一個 Swift 類和 Objective-C 類,然后使用 Xcode 模板新建一個初始化方法后,我們可以得到如下代碼:
@implementation BlogInitOC
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
@end
class BlogInit: NSObject {
override init() { // 需要手動添加 override 關鍵字
}
}
對比發現,Swift 的初始化代碼需要加上 override 關鍵字,方法內部沒有調用 super 的 init 方法,并且沒有 return 語句。雖然如此,但此時編譯是可以通過的。
接著,分別給這兩個類加上一個屬性變量 param:
@interface BlogInitOC ()
@property (nonatomic, strong) NSString *param;
@end
@implementation BlogInitOC
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
@end
class BlogInit: NSObject {
let param: String
override init() {
}
}
情況變了,Objective-C 類一切正常,而 Swift 類提示了一條錯誤 Property 'self.param' not initialized at implicitly generated super.init call
,意思很明確,param 參數沒有在隱式生成 super.init 調用之前完成初始化
。原來 Swift 中并不是不調用 super.init,而是為了方便開發者由編譯器完成了這一步,但是要求開發者在初始化方法中調用 super.init 之前完成成員變量的初始化。
修改后的代碼如下:
class BlogInit: NSObject {
let param: String
override init() {
self.param = "zhaohui"
// super.init() // 可不寫,編譯器隱式生成
}
}
對于需要修改父類中成員變量值的情況,我們需要在調用 super.init 之后再進行修改,代碼如下:
class Cat {
var name: String
init() {
name = "cat"
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
super.init()
name = "tiger"
}
}
因此 Swift 中類的初始化順序可以總結如下:
- 初始化自己的成員變量,必須;
- 調用父類初始化方法,如無需第三步,則這一步也可省略;
- 修改父類成員變量,可選。
這里補充說明兩點:
- 使用 let 聲明的常量是可以在初始化方法中進行賦值的,這是編譯器所允許的,因為 Swift 中的 init 方法只會被調用一次,這與 Objective-C 不同;
- 即使成員變量是可選類型,如:
let param: String?
,仍然是需要進行初始化的。
關鍵詞
看完上面這部分,好像 Swift 初始化也沒什么,不過是語法上一些變化,不過當我們按照曾經 Objective-C 的習慣添加類間繼承關系、自定義初始化方法等,問題又來了。
先來看下面這個例子:
class CustomView: UIView {
let param: Int
override init() { // error 1
self.param = 1
super.init() // error 2
}
} // error 3
好奇怪,我們只是將父類從 NSObject
修改為 UIView
,竟然收到3條錯誤:
- Initializer does not override a designated initializer from its superclass;
- Must call a designated initializer of the superclass 'UIView';
- 'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'。
稍等,再看一個例子:
class CustomView: UIView {
convenience init(param: Int, frame: CGRect) {
super.init(frame: frame) // error
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
這時,我們又得到了一條新的錯誤:
Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')
前面兩個例子中,我們看到了關鍵字 designated
、convenience
和 required
,理解了這幾個關鍵字也就能幫助我們理解整個初始化過程了。
designated
看到 designated
,我們很容易聯想到 Objective-C 中 NS_DESIGNATED_INITIALIZER
,它們的含義比較接近,都是用來設置指定初始化器,關于 Objective-C 中的用法,請參閱《正確編寫Designated Initializer的幾個原則》,下面我們主要討論 Swift 中的 designated
。
在 Apple 的官方文檔中講到,Swift 定義了兩種類初始化器類型,用來保證所有成員屬性能夠獲得一個初始化值,即 designated initializers
和 convenience initializers
。對于 designated initializers
的定義如下:
Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.
加粗部分是幾處關鍵的描述:
- primary initializers:designated initializers 是一個類的主初始化器,理論上來說是一個類初始化的必經之路(注:不同的初始化路徑可能調用不同的 designated initializers);
- fully initializes all properties:這點很明確,必須在 designated initializers 中完成所有成員屬性的初始化;
- calls an appropriate superclass initializer:需要調用合適的父類初始化器完成初始化,不能隨意調用。
下面我們結合前面的 Sample-1
進行解釋:
class CustomView: UIView {
let param: Int
override init() { // error 1
self.param = 1
super.init() // error 2
}
} // error 3
在 Swift 中,designated initializers 的寫法和一般的初始化方法無異,Sample-1
中,我們試圖去 override init
,可以理解為我們就是在 override
一個 designated initializers,然后我們收到了錯誤 Initializer does not override a designated initializer from its superclass,可見我們并沒有找到合適的 designated initializers,我們進入父類 UIView
,可以看到下面兩個初始化方法:
public init(frame: CGRect)
public init?(coder aDecoder: NSCoder)
原來,這兩個類才是父類的 designated initializers,那我們改改試試:
class CustomView: UIView {
let param: Int
override init(frame: CGRect) { // error 1 fixed
self.param = 1
super.init() // error 2
}
} // error 3
果然,error 1 沒了,由此也可以看出,我們去 override
一個不是 designated initializers
的初始化器不滿足定義中所說的 primary initializers,這就可能導致這個初始化器不被執行,成員變量沒有初始化,這樣創建的“半成品”實例可能存在一些不安全的情況。
第二條 fully initializes all properties,這點我們并沒有犯錯,因為我們已經初始化了 CustomView
類中引入的 param
變量。
第三條 calls an appropriate superclass initializer 很明顯就對應了 error 2,我們 override init(frame: CGRect)
,那我們就必須調用對應的父類初始化方法,修改如下:
class CustomView: UIView {
let param: Int
override init(frame: CGRect) { // error 1 fixed
self.param = 1
super.init(frame: frame) // error 2 fixed
}
} // error 3
再來看 error 3:'required' initializer 'init(coder:)' must be provided by subclass of 'UIView',這條錯誤提示我們 init(coder:)
是一個 'required' initializer,子類必須提供,那什么是 required 呢?
required
對于 required,官方給出了一句說明:
Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.
意思很明白,通過添加 required 關鍵字強制子類對某個初始化方法進行重寫。前面的 error 3 中,init(coder:)
正好對應了父類 UIView
中的第二個初始化方法,所以想要修復這個錯誤,就需要重寫 init(coder:)
。
其實,在 Xcode 中,雙擊這個錯誤就會幫我們插入這個方法,修復后代碼如下:
class CustomView: UIView {
let param: Int
override init(frame: CGRect) { // error 1 fixed
self.param = 1
super.init(frame: frame) // error 2 fixed
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
} // error 3 fixed
這樣,我們修復了 error 3,不過插入的這個方法很奇怪,方法體里直接寫 fatalError("init(coder:) has not been implemented")
,那豈不是走到這里就 fatal 了,這不是坑我們嗎?!
前文中我們講了,designated initializers 是一個類的主初始化器,理論上來說是一個類初始化的必經之路(注:不同的初始化路徑可能調用不同的 designated initializers),其實,這個 init(coder:)
與 init(frame: frame)
就是不同的初始化路徑,當我們使用 xib 方式初始化一個 view 時,就會走到 init(coder:)
。此時,如果我們沒有真正實現這個方法,就會出現 fatal crash,如下圖所示:
所以到目前為止,我們仍然沒有提供一套完整的、安全的初始化方法,需要繼續補全 init(coder:)
方法,以覆蓋全部可能的初始化流程:
class CustomView: UIView {
let param: Int
override init(frame: CGRect) {
self.param = 1
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
self.param = 1
super.init(coder: aDecoder)
}
}
這樣,我們就完成了一個 UIView
子類的初始化代碼。
convenience
在 Apple 的官方文檔對 convenience initializers
的定義如下:
Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.
convenience initializers
是對類初始化方法的補充,用于為類提供一些快捷的初始化方法,可以不創建這類方法,但如果創建了,就需要遵循原則:call a designated initializer from the same class,那么回到前文的 Sample-2:
class CustomView: UIView {
convenience init(param: Int, frame: CGRect) {
super.init(frame: frame) // error
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
這里我們得到的錯誤,正好匹配了上面的原則:
Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')
看來我們需要調用該類自己的 designated initializer
,那么我們應該 override init(frame: CGRect)
,然后修改 convenience init(param: Int)
中的 super 為 self:
class CustomView: UIView {
convenience init(param: Int, frame: CGRect) {
self.init(frame: frame) // error fixed
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
好啦,這下沒錯了!接著,我要用一個成員變量把 param
的值存起來:
class CustomView: UIView {
var param: Int
convenience init(param: Int, frame: CGRect) {
self.param = param // error
self.init(frame: frame)
}
override init(frame: CGRect) {
super.init(frame: frame) // error
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
又出來兩個錯誤:
- Use of 'self' in property access 'param' before self.init initializes self
- Property 'self.param' not initialized at super.init call
第二個錯誤我們清楚,是需要在調用 super.init
之前初始化本類成員屬性。第一個錯誤其實,這是 Swift 編譯器提供的安全檢查,文檔原文如下:
A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initializer assigns will be overwritten by its own class’s designated initializer.
原來 Swift 防止 convenience initializers
中賦值之后又被該類自己的 designated initializer
覆蓋而做了檢查,因此,正確的方式應該是調用該類的其他初始化方法之后再修改屬性值,最終修改如下:
class CustomView: UIView {
var param: Int
convenience init(param: Int, frame: CGRect) {
self.init(frame: frame)
self.param = param // error fixed
}
override init(frame: CGRect) {
self.param = 0 // error fixed
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
self.param = 0
super.init(coder: aDecoder)
}
}
小結
對于 Swift 中的初始化方法,總結如下:
- 子類中初始化方法必須覆蓋全部初始化路徑,以保證對象完全初始化;
- 子類中
designated initializer
必須調用父類中對應的designated initializer
,以保證父類也能完成初始化; - 子類中如果重寫父類中
convenience initializer
所需要的全部 init 方法,就可以在子類中使用父類的convenience initializer
了; - 子類如果沒有定義任何
designated initializer
,則默認繼承所有父類的designated initializer
及convenience initializer
; - 子類中必須實現的
designated initializer
,可以通過添加required
關鍵字強制子類重寫其實現,以保證依賴該方法的convenience initializer
始終可以使用; -
convenience initializer
必須調用自身類中的其他初始化方法,并在最終必須調用一個designated initializer
; - 在構造器完成初始化之前, 不能調用任何實例方法,或讀取任何實例屬性的值,
self
本身也不能被引用。
看上去 Swift 中對初始化過程添加了很多“規矩”,開發上繁瑣了不少,但是卻更有利于幫助我們開發更規范、更安全的初始化方法,從而減少一些潛在的問題,所以掌握這些“規矩”是非常有用且值得的。
可失敗初始化器
可失敗初始化器(Failable Initializers
),即可以返回 nil
的初始化方法,這在 Objective-C 的初始化過程中本來就支持,但這種支持反而導致邏輯上的模糊,什么時候返回 nil
其實我們并不明確,而 Swift 對這些情況進行了明確。
官方文檔對 Failable Initializers
的定義如下:
A failable initializer creates an optional value of the type it initializes. You write return nil within a failable initializer to indicate a point at which initialization failure can be triggered.
很容易理解,就是將初始化返回值變成 optional value(在 init
后面加上 ?
),并在不滿足初始化條件的地方 return nil
,這樣,我們通過調用處判斷是否有值即可知道是否初始化成功。
我們以官方例子進行解釋:
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
CartItem
類的初始化方法先對傳入參數 quantity
的值進行判斷,小于 1 則為無效參數,然后 return nil
(初始化失敗),大于或等于 1 則繼續調用父類 Product
的初始化方法,再次判斷傳入參數 name
,為空則 return nil
(初始化失敗),否則繼續初始化。
這樣,我們通過下面幾種不同參數進行初始化,即可得到不同的初始化結果:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
print("Unable to initialize zero shirts")
}
// Prints "Unable to initialize zero shirts"
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product"
總的來說,可失敗初始化器的設定,是在保證安全性的基礎上提供了邏輯上更清晰的初始化方式。Failable Initializers
所有的結果都將是 T?
類型,通過 Optional Binding
方式,我們就能知道初始化是否成功,并安全地使用它們了。
注:本文所有描述均針對類類型初始化,對于結構體或枚舉類型基本類似,還有一些其他特性大家可以參考官方文檔進行學習。