從 Swift 初始化說起

原文地址: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 中類的初始化順序可以總結如下:

  1. 初始化自己的成員變量,必須
  2. 調用父類初始化方法,如無需第三步,則這一步也可省略
  3. 修改父類成員變量,可選

這里補充說明兩點:

  1. 使用 let 聲明的常量是可以在初始化方法中進行賦值的,這是編譯器所允許的,因為 Swift 中的 init 方法只會被調用一次,這與 Objective-C 不同;
  2. 即使成員變量是可選類型,如: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條錯誤:

  1. Initializer does not override a designated initializer from its superclass
  2. Must call a designated initializer of the superclass 'UIView'
  3. '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')

前面兩個例子中,我們看到了關鍵字 designatedconveniencerequired,理解了這幾個關鍵字也就能幫助我們理解整個初始化過程了。

designated

看到 designated,我們很容易聯想到 Objective-C 中 NS_DESIGNATED_INITIALIZER,它們的含義比較接近,都是用來設置指定初始化器,關于 Objective-C 中的用法,請參閱《正確編寫Designated Initializer的幾個原則》,下面我們主要討論 Swift 中的 designated

在 Apple 的官方文檔中講到,Swift 定義了兩種類初始化器類型,用來保證所有成員屬性能夠獲得一個初始化值,即 designated initializersconvenience 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.

加粗部分是幾處關鍵的描述:

  1. primary initializers:designated initializers 是一個類的主初始化器,理論上來說是一個類初始化的必經之路(注:不同的初始化路徑可能調用不同的 designated initializers);
  2. fully initializes all properties:這點很明確,必須在 designated initializers 中完成所有成員屬性的初始化;
  3. 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:) fatal

所以到目前為止,我們仍然沒有提供一套完整的、安全的初始化方法,需要繼續補全 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")
    }
}

又出來兩個錯誤:

  1. Use of 'self' in property access 'param' before self.init initializes self
  2. 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 中的初始化方法,總結如下:

  1. 子類中初始化方法必須覆蓋全部初始化路徑,以保證對象完全初始化;
  2. 子類中 designated initializer 必須調用父類中對應的 designated initializer,以保證父類也能完成初始化;
  3. 子類中如果重寫父類中 convenience initializer 所需要的全部 init 方法,就可以在子類中使用父類的 convenience initializer 了;
  4. 子類如果沒有定義任何 designated initializer,則默認繼承所有父類的 designated initializerconvenience initializer
  5. 子類中必須實現的 designated initializer,可以通過添加 required 關鍵字強制子類重寫其實現,以保證依賴該方法的 convenience initializer 始終可以使用;
  6. convenience initializer 必須調用自身類中的其他初始化方法,并在最終必須調用一個 designated initializer
  7. 在構造器完成初始化之前, 不能調用任何實例方法,或讀取任何實例屬性的值,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 方式,我們就能知道初始化是否成功,并安全地使用它們了。

注:本文所有描述均針對類類型初始化,對于結構體或枚舉類型基本類似,還有一些其他特性大家可以參考官方文檔進行學習。


參考資料

  1. The Swift Programming Language: Initialization
  2. Swift 類構造器的使用
  3. 初始化方法順序
  4. 正確編寫Designated Initializer的幾個原則
  5. DESIGNATED,CONVENIENCE 和 REQUIRED
  6. 初始化返回 NIL
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容