iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 20
0
Software Development

Swift 菜鳥的30天系列 第 20

Day-20 Swift 語法(16) - Class 與 初始化器

Class 的繼承與初始化器

所有 Class 的儲存屬性 (包括從父類繼承的任何屬性ㄉ) 都必須在初始化的期間分配一個初始值。Swift 為 Class 類型定義了兩種初始化器,去幫助確認所有的儲存屬性接收一個初始值。這些被稱為指定初始器 (designated initializers) 和便捷初始化器 (convenience initializers)。

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

指定初始化器是 class 中主要的初始化器,指定初始化器完全初始化由該 class 引用的所有屬性,並且調用合適的父類初始化器去繼續初始化程序給父類鏈。 class 通常只有很少數的指定初始化器,而一個 class 通常只有一個指定初始化器,指定初始化器是初始化開始並持續初始化過程到父類鏈的“管道”點。

便捷初始化器是次要支持 Class 的初始化器,你可以在相同的 class 裡定義一個便捷初始化器來調用一個指定的初始化器作為便捷初始化器來給指定初始化器設置默認參數,你也可以為具體的使用情況或輸入的值類型定義一個便捷初始化器從而創建這個 class 的實例。

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

class 的指定初始化器的寫法與簡單的值類型初始化器相同:

init(parameters) {
    statements
}

便捷初始化器的也是一樣的寫法,但是在 init 關鍵字之前放了 convenience 修飾符,並用一個空格分隔:

convenience init(parameters) {
    statements
}

Class 類型的初始化器委託

為了簡化指定初始化器與便捷初始化器之間的關係,Swift 在初始化器之間委託調用有下面三個規則:

  1. 指定的初始化器必須從它的直系父類調用指定初始化程序。
  2. 便捷初始化器必須調用同一個 class 中的另一個初始化器。
  3. 便捷初始化器最終必須調用一個指定的初始化器。

用一個簡單的方式記住這些:

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

我們用圖片來描述上述這些規則:
https://ithelp.ithome.com.tw/upload/images/20180108/20107701OAdBbWhIJG.png

圖中的父類有一個指定初始化器和兩個便捷初始化器。一個便捷初始化器調用另一個便捷初始化器,而後者又調用了指定初始化器。這滿足了上邊的規則2、3,由於父類本身沒有其他父類,所以不需要規則1。

圖中的子類有兩個指定初始化器和一個便捷初始化器。這個便捷初始化器必須調用兩個指定初始化器其中一個,因為他只能中相同 class 中調用初始化器,目前到這邊它符合了規則2、3,接著這兩個指定初始化器又必須從父類調用一個指定初始化器,滿足規則1的要求。

這些規則不會影響到使用者如何創建每個 class 的實例,上圖中任何的初始化器都可以用來創建它們所屬 class 完全初始化的實例。這些規則只會影響你如何編寫的初始化器的實現。

下面我們用一張更複雜的 class 的結構圖,它演示了指定初始化器是如何在此層級結構中充當”管道”作用,在 class 的初始化鏈上簡化了 class 之間的內部關係:

https://ithelp.ithome.com.tw/upload/images/20180108/201077013kLqhoS2lh.png


兩階段初始化

Swift 中的 Class 初始化是一個兩階段的過程。第一階段中,每個儲存的屬性被引入類為分配了一個初始值。一旦確定了每個存儲屬性的初始狀態,那麼第二階段就開始了,每個 class 都有機會在新的實例準備使用之前來自定義它的存儲屬性。

使用兩階段初始化能使初始化安全,同時仍然為 class 層次結構中的每個 class 提供完全的靈活性。兩階段初始化防止在初始化之前訪問屬性值,也防止其他初始化器意外的將屬性值設為不同的值。

Swift 的編譯器執行四個有用的安全檢查(safety check),來確保兩階段初始化完成沒有錯誤:

  1. 指定的初始化器必須在它委託給父類初始化器前,它的類所引入的所有屬性都被初始化了。
    如上所述,一個對象的內存只有在其所有儲存屬性確定之後才能完全初始化。為了滿足這一規則,指定初始化器必須保證它自己的屬性在委託之前先完成初始化。
  2. 指定初始化器必須先向上委託父類初始化器,然後才能為繼承的屬性設置新的值。如果沒有這麼做,指定初始化器賦予的新值將被父類中的初始化器所覆蓋。
  3. 便捷初始化器必須先委託相同 class 中的其它初始化器,然後再為任意屬性賦新值(包括同 class 中定義的屬性)。如果沒這麼做,便捷初始化器賦予的新值將被自己類中其它指定初始化器所覆蓋。
  4. 初始化器不能調用任何實例的方法,不能讀取任何實例的值,也不能引用 self 作為值。class 的實例直到第一節段結束才是完全有效的,屬性只能被訪問,方法只能被調用,直到第一階段結束的時候,這個類實例才被看做是合法的。

基於上面的四個安全檢查,下面說明兩階段內初始化過程:

*階段一

  • 在一個 class 中調用指定或是便捷初始化器。
  • 為 class 的新實例分配內存,內存還沒初始化。
  • 該 class 的指定初始化器確認由該 class 引入的所有儲存屬性都有一個值,這些儲存的屬性的內存現在被初始化。
  • 指定的初始化器轉到父類的初始化器,為自己的儲存屬性執行相同的任務。
  • 這個調用父類初始化器的過程將沿著初始化器鏈一直向上進行,直到到達初始化器鏈的最頂部;
  • 一但到達鏈的最頂端,並且鏈中的最後一個 class 以確保所有儲存的屬性都有一個值,則實例的內存被認為是完全* 初始化的,此時就完成階段一。

*階段二

  • 從鏈的頂部開始,鏈中的每個指定初始化器都可以選擇進一步自定義實例。初始化器現在可以訪問 self ,並可以修改他的屬性,調用它的實例方法等等。
  • 最後,鏈中任何的便捷初始化器都可以選擇自定義實例並使用 self。

下圖是第一階段如何查詢假設的子類和父類的初始化調用:
https://ithelp.ithome.com.tw/upload/images/20180108/20107701ZQ48veXvjM.png

圖中,初始化過程從一個子類的便捷初始化器開始。這時便捷初始化器還不能修改任何屬性。它委託給了同一 class 裡的指定初始化器。
指定初始化器確保所有的子類屬性都有一個值,然後它調用父類的指定初始化器來沿著初始化器鏈一直往上完成父類的初始化過程。
父類的指定初始化器確保所有的父類屬性都有值。由於沒有更多的父類來初始化,也就不需要更多的委託。
一旦父類中所有屬性都有初始值,它的內存就被認為完全初始化了,第一階段完成。

下圖是第二階段的初始化過程:
https://ithelp.ithome.com.tw/upload/images/20180108/20107701F1JxDdDB1o.png

此時,父類的指定初始化器有機會進一步自定義實例(儘管不必要)。
一旦父類的指定初始化器完成了調用,子類的指定初始化器就可以執行額外的定制(同樣,儘管沒有這種必要)。
最後,一旦子類的指定初始化器完成,最初調用的便捷初始化器將會執行額外的自定義操作。


初始化器的繼承和重寫

如果你想要自定義的子類提供一個或多個相同的初始化作為他的父類,你可以在子類中提供這些初始化的自定義實現。
當你編寫一個子類的初始化器與一個父類指定初始化器相匹配時,你正有效的提供對指定的初始化器重寫。因此,你必須在子類的初始化程序定義之前編寫 override 修飾符。即使是自動提供的默認初始化器你也可以重寫。

與重寫屬性、方法、下標一樣,override修飾符會提示 Swift 檢查父類是否有一個匹配的指定初始值設定項被重寫,並驗證你的重寫初始化器的參數是否按照預期指定了。

當重寫父類指定初始化器時,你必須寫 override 修飾符,即使子類的初始化器的實現是一個便捷初始化器。

相反的,如果你編寫一個父類便捷初始化器相匹配的子類初始化器,那麼父類的初始化器不能由你的子類直接調用。因此你的子類不是提供父類的初始化的重寫,所以在提供父類的便捷初始化器的匹配實現時,不需要 override 修飾符。

我們下面創建一個名為 Animals 的基類(base class),宣告一個名為 leg 的儲存屬性,類型為 Int ,默認值為 0。之後 leg 透過 legOfAnimal 的計算屬性來創建一個 String 類型的字符串描述:

class Animals {
    var leg = 0
    var legOfAnimal:String {
        return "有\(leg)隻腳"
    }
}

Animals 這個 class 只為它的存儲屬性提供了默認值,並且沒有提供自定義的初始化器。因此,它會自動收到一個默認初始化器。默認初始化器(如果可用)指的總是 class 的指定初始化器,也可以用來創建一個新的 Animals 實例, leg 默認為 0 :
https://ithelp.ithome.com.tw/upload/images/20180108/20107701nbViBg042B.png

下面我們定義了一個名為 Bird 繼承 Animal 的子類:

class Bird:Animals {
    override init() {
        super.init()
        leg = 2
    }
}

Bird 定義了一個自定義初始化器 init()。這個指定初始化器和 Bird 的父類的指定初始化器相匹配,所以 Bird 中的指定初始化器需要加上 override 修飾符。
Bird 類的 init() 初始化器以調用 super.init () 開始,這個方法作用是調用父類的初始化器。這樣可以確保 Bird 在修改屬性之前它所繼承的屬性 leg 能被 Animals 類初始化。在調用 super.init() 之後,一開始的 leg 值會被我們設置的值 2 給替換。

https://ithelp.ithome.com.tw/upload/images/20180108/201077017ywZfSFDTW.png

子類可以在初始化時修改繼承的變量屬性,但是不能修改繼承過來的常量屬性。


自動初始化器的繼承

默認情況下,子類不會繼承其父類的初始化器。在特定的條件下父類初始化器是可以被自動繼承的。實際上,這意味著在許多狀況下你不必重寫父類初始化器,只要可以安全操作,你就可以毫不費力地繼承父類的初始化器。

假如你為在子類中引入的任何新屬性提供默認值,則遵循下面兩條規則:

*規則一
如果你的子類沒有定義任何指定初始化器,它將自動繼承所有父類的初始化器。
*規則二
如果你的子類提供所有父類指定初始化器的實現,他會按照規則一繼承它們,或者它會提供一個字定義實現作為其定義的一部分,那麼它將會自動繼承所有父類便捷初始化器。就算你的子類添加了更多的便捷初始化器,這些規則依然適用。


指定和便捷初始化器的操作

下面我們創建一個 Stationery 文具的基類,我們引入了一個名為 name 的 String 屬性,還提供了兩個創建 Stationery 實例的初始化器:

class Stationery{
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init(){
        self.init(name: "未知")
    }
}

因為這個 class 沒有默認成員初始化器,所以 class 提供了一個接受單一參數的指定初始化器叫做 name 。使用這個初始化器可以使用一個具體的名稱來創建新的 Stationery 實例,我們可以提供他參數傳值到其中,又或者使用默認的便捷初始化提供的默認值表示:

https://ithelp.ithome.com.tw/upload/images/20180108/20107701zAwPy3anC7.png

接下來我們在創建一個 Tool 的類,繼承我們的 Stationery,引入了名為 quantity 的屬性,類型為Int(以及從 Stationery 繼承過來的 name 屬性),並且定義了兩個初始化器來創建 Tool 實例:

class Tool:Stationery {
    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 只有一個指定初始化器,init(name:String , quantity:Int),它可以用來輸入新的 Tool 實例中所有的屬性。這個初始化器一開始先將傳入的 quantity 實際參數賦值給 quantity 屬性,這個屬性也是唯一一個通過 Tool 引入的新屬性。然後,初始化器將向上委託給父類 Stationery 的 init(name: name) 初始化器。

我們同樣定義了一個便捷初始化器,init(name: String),它只透過 name 來創建 Tool 的實例。這個便捷初始化器假設 Tool 中沒有設定數量實例的 quantity 值都為 1 。便捷初始化器的定義讓 Tool 實例創建的更方便更快速,並且當創建多個單數實例時可以避免代碼冗長。 Tool 類提供的 init(name: String) 便捷初始化器接收與 Stationery 中的指定初始化器 init(name: String) 為相同的形式參數。因為這個便捷初始化器從它的父類重寫了一個指定初始化器,所以我們必須加上 override 修飾符。

儘管 Tool 提供了 init(name: String) 初始化器作為一個便捷初始化器,然而 Tool 類為所有的父類指定初始化器提供了實現。因此,Tool 類也自動繼承了父類所有的便捷初始化器。

在這個範例中, Tool 的父類是 Stationery ,它只有一個 init()便捷初始化器。因此這個初始化器也被 Tool 類繼承。這個繼承的 init() 函數和 Stationery 提供的是一樣的,除了它是委託給 Tool 版本的 init(name: String) 而不是 Stationery 版本。

我們可以用下面三種方式依照你的需求創建不同的實例:

let tool1 = Tool() 
// name = 未知 , 數量為默認值為 1
let tool2 = Tool(name: "pencil") 
// name = pencil , 數量為默認值為 1
let tool3 = Tool(name: "ruler", quantity: 3) 
// name = ruler , 數量為 3

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180108/201077019BpRJpxLhM.png


上一篇
Day-19 Swift 語法(15) - 初始化 Initialization
下一篇
Day-21 Swift 語法(17) - 可失敗初始化器
系列文
Swift 菜鳥的30天30

尚未有邦友留言

立即登入留言