iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0

我們今天繼續我們的初始化器

Class 的繼承和初始化器

Class 裡面的所有存儲屬性(包括從父類繼承的人和屬性)都必須初始化的期間中設置初始值。
Swift 裡面為 Class 提供了兩種初始化器,來確保實例中所有存儲屬性都能獲得初始值,分別是指定初始化器跟便利初始化器。

指定初始化器和便利初始化器

指定初始化器是最主要的初始化器。指定初始化器會初始化 Class 所提供的所有屬性,並調用合適的父類初始化器讓初始化器沿著父類鏈網上進行。

每一個 Class 都至少擁有一個指定初始化器,只有在某些情況下 Class 繼承了父類裡面的指定初始化器。

便利初始化器是 Class 裡面比較次要的初始化器,屬於輔助型的初始化器。可以定義便利初始化器來調用同一個 Class 裡面的 指定初始化器,並為部分提供初始默認值。或是用便利初始化器創建一個特殊用途或特定輸入值得實例。

指定初始化器和便利初始化器語法

指定初始化器

init(parameters) {
    statements
}

便利初始化器

convenience init(parameters) {
    statements
}

Class 初始化器委託

為了簡化指定初始化器和便利初始化器之間的調用關係,Swift 初始化器之間的委託調用遵循以下三點:

  1. 指定初始化器必須從它的直系父類調用指定初始化器
  2. 便利初始化器必須調用同 Class 中定義的其他初始化器
  3. 便利初始化器最後必須調用指定初始化器

也可以用簡單的方式記住這些

  • 指定初始化器必須總是向上委託
  • 便利初始化器必須總是橫向委託

官方有圖片來描述這些規則

如圖片所示,父類中包含一個指定初始化器和兩個便利初始化器。其中一個便利初始化器調用了另外一個便利初始化器,而後者又調用了唯一的指定初始化器。而這滿足了 2 和 3 的規則。而父類本身沒有其他父類所以不用符合規則 1 。

子類中包含兩個指定初始化器和一個便利初始化器。便利初始化器必須調用兩個指定初始化器中的任意一個,因為他只能調用同一個 Class 裡面的其他初始化器。這滿足了 2 和 3 規則。而兩個指定初始化器必須調用父類中唯一的指定初始化器,這滿足規則 1 。

下圖有更複雜的 Class 層級結構。他演示了指定初始化器是如何在 Class 層級中充當管道使用,在 Class 的初始化鏈上簡化了 Class 之間的相互關係。

兩段式初始化過程

Swift 中 Class 的初始化過程有兩個階段,階段一,Class 中的的每個存儲屬性賦予一個初始值。當存儲屬性都被賦值後,才會進入第二階段,給 Class 可以在新實例準備使用前自定義他們的存儲屬性。

兩段式初始化過程的使用讓初始化過程更安全,同時在整個 Class 層級結構中給予了每個 Class 完全的靈活性。兩段式初始化過程可以防止屬性值在初始化之前就被訪問,有可以防止屬性被另一個初始化器意外的賦予不同的值。

Swift 會進行 4 種有效的安全檢查,以確保兩段式初始化過程不出錯

  1. 指定初始化器必須保證他在 Class 所有屬性都必須先初始化完成,之後才能將其他初始化任務向上代理給父類中的初始化器。

如上所述,一個對象的內存只有其所有存儲屬性確定之後才能完全初始化。為了滿足這一條規則,指定初始化器必須保證他在 Class 的屬性他往上代理時就完成初始化。

  1. 指定初始化器必須在為繼承的屬性設置之前向上代理調用父類初始化器。如果沒這樣做,指定初始化器賦予的新值將被父類中的初始化器所覆蓋。
  2. 便利初始化器必須為任意屬性(包含所有同 Class 中定義的)賦予新值前代理調用其他初始化器。如果沒這樣做,便利初始化器賦予的新值將被該 Class 的指定初始化器覆蓋。
  3. 初始化器在完成第一階段初始化前,不能調用任何實例方法,不能讀取任何實例屬性的值,不能引用 self 作為一個值。

Class 的實力在第一階段結束以前並不是完全有效的。只有第一階段結束後, Class 的實力才是有效的,才能訪問屬性和調用方法。

以下是基於上述安全檢查的兩段式初始化過程展示:

階段一:

  • Class 的某個指定初始化器或便利初始化器被調用
  • 完成 Class 新實例內存的分配,但此時內存還沒有被初始化
  • 指定初始化器確保其所在 Class 引入的所有存儲屬性都已賦予初始值。存儲屬性所屬的內存完成初始化
  • 指定初始化器切換到父類的初始化器,對其存儲屬性完成相同的任務
  • 這個過程沿著 Class 的繼承鏈一直往上執行,直到到達繼承鏈的最頂部
  • 當到達了繼承鏈的最頂部,而且繼承鏈的最後一個 Class 確保所有存儲屬性都已經賦值,這個實力的內存被認為已經完全初始化,第一階段完成。

階段二:

  • 從繼承鏈頂部往下,繼承鏈中每個 Class 的指定初始化器都有機會進一步自定義實例。初始化器此時可以訪問 self 、 修改他的屬性並調用實力方法等等。
  • 最終,繼承鏈中任意的便利初始化器都有機會自定義實例和使用 self

下圖展示了在假定的子類和父類之間的初始化階段一:

在途中,初始化過程對子類中一個便利初始化器的調用開始。這個便利初始化器此時不能修改任何屬性,他會代理到該類中的指定初始化器。

如安全檢查1. 所示,指定初始化器將確保所有子類的屬性都有值。然後他將調用父類的指定初始化器。並沿著繼承鏈一直往上完成父類的初始化過程。

父類中的指定初始化器確保了所有父類的束性都有值。由定喔服類要初始化,有就無需繼續向上代理。

一但父類中都有了初始值,實力的內存才是被認為有完全初始化,第一階段完成。

以下展示了初始化過程的階段二:

父類中指定初始化器現在有機會進一步自定義實例

一但父類中的指定初始化器完成調用,子類中的指定初始化器可以執行更多自定義操作

最終,一但子類的指定初始化器完成調用,最開始被調用的便利初始化器可以值性更多的自定義操作。

初始化器的繼承和重寫

假如你希望自定義中的子類能提供一個或多個跟父類相同的初始化器,你可以在子類中提供這些初始化器自定義實現。
當你在編寫一個和父類中指定初始化器相匹配的子類初始化器時,你實際上就是在重寫父類的這個指定初始化器。因此,你必須在定義子類初始化器時加上 override 。即時你重寫的是翕動自動提供的默認初始化器,也需要加上 override 。

正如重寫屬性一樣,方法或者是下標 override 會讓編譯器去檢查父類中是否有匹配的指定初始化器,並驗證初始化器參數是否有被指定。

相反的如果你編寫一個和父累便利初始化匹配的子類初始化器,由於子類不能直接調用父類的便利初始化器,因此,你的子類並未對一個副類初始化器提供重寫,結果是你在子類中 重寫 一個父類便利初始化器時不需要加 override 。

接下來我們來看官方的例子

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Vehicle 這個 Class 只有給存儲行屬性默認值,沒有特別定義自己的初始化器。因此他就是有一個系統給的默認初始化器。

let vehicle = Vehicle()
print("Vehicle: \(vehicle,description)")
// Vehicle: 0 wheel(s)

上面實例化了 Vehicle
下面的例子讓 Bicycle 繼承了 Vehicle

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

依照前面說的,如果子類要自定義自己的初始化器,就要對父類的初始化器做重寫,所以要加上 override 在前面進行修改。super.init() 就是要先知道父類裡面的 init 是長怎樣。
所以實例化之後會像下面這樣

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// 打印“Bicycle: 2 wheel(s)”

初始化器的自動繼承

如上所述,子類在默認狀況下不會繼承父類的初始化器。但是如果滿足特定條件,父類初始化器是可以自動被繼承的。這樣你就不用重寫父類的初始化器並可以在安全的狀況下以最小的代價繼承父類的初始化器。

假設你為子類中引入的新屬性都提供了默認的值。以下兩個規則都適用:

  1. 如果子類沒有定義任何指定初始化器,他將自動繼承父類所有指定初始化器
  2. 如果子類提供了所有服類指定初始化器的實現,無論是通過上面規則一繼承過來的還是提供了自定義實現,都將自動繼承父類所有的便利初始化器

即時你在子類添加了更多的便利初始化器,這兩條規則都仍然適用。

指定初始化器和便利初始化器操作

下面來看官方的操作過程例子

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

這個 Food 的 Class 裡面兩個初始化器和一個 name 的 String 類型的存儲類型。

下圖中展示了 Food 初始化器鏈

Class 沒有默認的逐一成員初始化器,所以 Food 提供了一個接受單一參數的 name 的指定初始化器。

let nameMeat = Food(name: "Bacon")
// nameMeat 的名字是 Bacon

Food 初始化器是一個指定初始化器,因為他裡面的存儲屬性都有被初始化。然後 Food 沒有父類所有不需要調用 super.init() 。

接下來我們看這個官方例子

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

下圖展示了這個 Class 的初始化器鏈

RecipeIngredinet 這個類擁有一個指定初始化器,可以來實例他裡面的所有屬性值。這個初始化器一開始先將傳入的 quantitiy 參數費與給 quantitiy 屬性,這個屬性也是唯一在 RecipeIngredinet 中新引入的屬性。初始化器向上代理到父類 Food 的 init(name: String)。

RecipeIngredinet 也定義了一個便利初始化器,他通過 name 來創建實例。這個便利初始化器假設任意的 quantity 為 1 ,所以不需要顯示的質量也可以創建出實例。為了不要重複創建多個 quantity 為 1 的實例,這個便利初始化器只是簡單的橫向代理到 Class 中的指定初始化器。

而 RecipeIngredinet 的便利初始化器使用了 Food 的指定初始化器,所以前面要加上 override 。

僅管 RecipeIngredinet 將父類的指定初始化器重寫,為了便利初始化器,但是他依舊提供了父類所有的指定初始化器的實現。所以他會自動繼承父類的所有便利初始化器。


好的,今天就先到這裡了


上一篇
30天的 iOS 修仙道路 (25)
下一篇
30天的 iOS 修仙道路 (27)
系列文
30天的 iOS 修仙道路 站穩腳步基礎篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言