Swift 程式語言 — Initialization (2)

讓我們繼續深入研究不同的初始化方式。

Jeremy Xue
Jeremy Xue ‘s Blog
18 min readJul 7, 2019

--

Photo by Markus Spiske on Unsplash

# 前言

讓我們接續上一篇文章,繼續深入瞭解更多的初始化方式吧!

# Class 的繼承和初始化

所有 class 的存儲屬性(包含該 class 從其 superclass 所繼承的任何屬性)都必須在初始化期間分配初始值。

Swift 為 class 類型定義了兩種初始化器,來幫助確保所有存儲屬性都接收初始值。這些被稱為指定初始化器( Designated initializers )和便捷初始化器( Convenience initializers )。

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

  • 指定初始化器:

指定初始化器是 class 的主要初始化器,指定初始化器完全初始化該 class 中引入的所有屬性,並且調用適當的 superclass 的初始化器來繼續 superclass 的初始化過程(superclass chain )。

Classes 往往只有很少的指定初始化器,並且一個 class 只有一個指定初始化器是很常見的。指定初始化器是初始發生的 “漏斗” ( funnel )點,初始化過程透過該點繼續 superclass 的初始化過程。

每個 class 必須至少有一個指定初始化器。在某些情況下,透過從 superclass 繼承一個或多個指定初始化器來滿足這個要求。我們在下方的自動初始化器繼承會提到。

  • 便捷初始化器:

便捷初始化器是次要的,是支援 class 的初始化器。你可以定義一個便捷初始化器,以便於從與便捷初始化器相同的 class 中調用指定初始化器,並且將某些指定初始化器的參數設置為默認值。

你還可以定義一個便捷初始化器,用於創建特定用途或輸入的 value type 類型創建該 class 的實例。

如果你的 class 不需要提供便捷初始化器。如果透過通用初始化器模式的快捷方式可以節省時間或使 class 的初始化意圖更清楚,那麼就可以創建便捷初始化器。

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

Class 的指定初始化器編寫方式與 value type 的簡單初始化器相同:

便捷初始化程序以相同的方式編寫,但在 init 關鍵字之前放置了 convenience 修飾符:

# Class type 的初始化程序委託

為了簡化指定和便捷初始化器之間的關係,Swift 對於初始化器之間的委託調用遵循下面三個規則:

  • 規則1:指定初始化器必須從其直接的 superclass 調用指定初始化器。
  • 規則2:便捷初始化器必須從同一個 class 調用另一個初始值器。
  • 規則3:便捷初始化器最終必須調用指定初始化器。

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

  • 指定初始化器必須總是向上委託( delegate up )。
  • 便捷初始化器必須總是橫向委託( delegate across )。

這些規則如下圖所示:

在這邊,在 superclass 有一個指定初始化器以及便捷初始化器。其中一個便捷初始化器調用另一個便捷初始化器,另一個便捷初始化器則是調用單個指定初始化器。這滿足上述的規則2和3。在這邊因為 superclass 本身沒有其他 superclass,因此規則1不適用。

而圖中的 subclass 有兩個指定的初始化器和一個便捷初始化器。便捷初始化器必須調用兩個指定初始化器之一,因為他只能從同一個 class 中調用另一個初始化器。這滿足上述的規則2和3。兩個指定的初始化器必須從 superclass 調用單個指定初始化器,來滿足上述的規則1。

NOTE
這些規則並不會影響 class 的使用者如何創建每個 class 的實例。上圖任何的初始化器都可以用於創建它們所屬的 class 完全初始化的實例。規則僅有影響你編寫 class 初始化器的實現方式。

下圖演示了四個 class 更為複雜的 class 層次結構。其說明了這個層次結構中的指定初始化器如何作為 class 初始化的 “漏斗” 點,簡化了鏈中 class 之間的相互間係。

# 兩段式初始化

Swift 中的 class 初始化是一個兩階段的過程。第一階段,每個存儲屬性都透過引入的 class 被分配一個初始值。一旦確定了每個存儲屬性的初始狀態,並且每個 class 都有機會在新實例被認為可以使用前,進一步自定義其存儲屬性。

使用兩段式初始化過程可以使初始化安全,同時仍然未 class 階層中的每個 class 提供完全的靈活度。兩段式初始化可防止在初始化屬性之前訪問它們,並且防止屬性值被另一個初始化器意外的設置為不同的值。

NOTE
Swift 的兩段式初始化過程類似於 Objective-C 中的初始化。主要區別在於第一階段,Objective-C 為每個屬性分配 0 或是 nil。Swift 的初始化流程更加靈活,因為它允許你設置自定義初始值,並且可以處理 0nil 不是有效默認值的類型。

Swift 的編輯器執行四個有用的安全檢查來確保兩階段初始化完成,並且沒有錯誤:

  • 安全檢查1
    指定初始化器必須確保在向上委託到 superclass 初始化器之前初始化該 class 引入的所有屬性。

如上所述,一旦知道了所有存儲屬性的初始狀態,就認為對象的記憶體被完全初始化。為了滿足這個規則,指定初始化器必須確保在鏈結束之前初始化其所有屬性。

  • 安全檢查2
    在為繼承的屬性分配值之前,指定的初始化器必須向上委託一個 superclass 初始化器。如果沒有,指定初始器分配的新值將會被 superclass 覆蓋,作為其自身初始化的一部分。
  • 安全檢查3
    在分配值任何屬性之前( 包括由同一個 class 定義的屬性 ),便捷初始化器必須委託給另一個初始化器。如果沒有,則便捷初始化器分配的新值將被其自己 class 的指定初始化器的覆蓋。
  • 安全檢查4
    初始化器在第一階段初始化完成之前,不能調用任何實例方法、不能讀取任何實例屬性的值,也將 self 作為值引用。

在第一階段結束之前,Class 實例不完全有效。只有在第一階段結束時知道 class 實例有效時,屬性只能被訪問、方法只能被調用。

根據上面的四個安全檢查,以下是兩段式初始化的方式:

  • 階段1
  • 階段2

下圖是階段1如何尋找假定的 subclass 和 superclass 的初始化調用:

在這個範例中,初始化始於 subclass 中便捷初始化器的調用。此便捷初始化器還不能修改任何屬性,其橫向委託給同個 class 中的指定初始化器。

根據安全檢查1,指定初始化器確保所有 subclass 的屬性都具有值。然後它在其 superclass 上調用指定初始化器來繼續初始化鏈。

Superclass 的指定初始化器確保所有 superclass 屬性都具有值。沒有其他 superclass 要初始化,因此不需要進一步委託。

只有 superclass 的所有屬性都具有初始值,就會認為其記憶體已全部初始化,並且階段一已完成。

下圖是相同的初始化過程在階段二的樣子:

Superclass 的指定初始化器現在有機會進一步自定義實例( 儘管它不需要 )。

一旦 superclass 的指定初始化器完成,Subclass 的指定初始化器就可以執行額外的字定義( 同樣的,儘管如此,它不需要 )

最後,一旦 subclass 的指定初始化器完成,最初調用的便捷初始化器可以執行其他自定義操作。

# 初始化器繼承和覆寫

與 Objective-C 中的 subclasses 不同,Swift 的 subclasses 默認不繼承其 superclass 的初始化器。Swift 的方法可以防止 superclass 中的簡單初始化器被更專用的 subclass 繼承,並用於創建未完全或正確初始化的 subclass 的新實例。

NOTE
Superclass 初始化器在某些情況下是會繼承的,但只有在安全和適合的情況下才會這麼做。

如果你希望自定義 subclass 與其 superclass 一起呈現一個或多個初始化器,則可以在 subclass 中提供這些初始化器的自定義實現。

當你編寫與 superclass 初始化器相匹配的 subclass 初始化器時,你實際上提供了對該指定初始化器的覆寫。因此,你必須在 subclass 的初始化器定義之前編寫 override 修飾符。即使你覆寫了自動提供的默認初始化器。

如同重寫屬性、方法和下標。override 修飾符的存在提示了 Swift 檢查 superclass 是否具有與要覆寫相匹配的指定初始化器,並驗證已覆寫初始化器的參數已按意圖指定。

NOTE
當覆寫 superclass 的指定初始化器時,總是編寫 override 修飾符,即使你的 subclass 的初始化器實現是便捷初始化器。

相反的,如果編寫與 superclass 便捷初始化器匹配的 subclass 初始化器,根據上面在 #Class type 的初始化程序委託 所描述的規則,永遠不能從 subclass 中直接調用 superclass 初始化器。因此,你的 subclass (嚴格來說)不提供 superclass 初始化器的覆寫。因此,在提供 superclass 便捷初始化器的匹配實現時,不要編寫 override 修飾符。

下面我們定義一個名為 Vehicle 的 base class 。這個 base class 宣告一個名為 numberOfWheels 的存儲屬性,其默認 Int 值為 0。numberOfWheels 屬性由名為 description 的計算屬性使用,來創建車輛特徵的 String 描述:

Vehicle 為其唯一的存儲屬性提供默認值,並且不提供任何自定義初始化器。因此,它會自訂接收默認初始化器。默認初始化器( 如果可用 )始終是 class 的指定初始化器,可用於創建 numberOfWheels0 的新 Vehicle 實例:

下一個範例定義了一個 Vehicle 的 subclass 稱為 Bicycle

Bicycle 定義了自定義的指定初始化器 init()。這個指定初始化器匹配來自 Bicycle 的 superclass 的指定初始化器。因為該初始化器的 Bicycle 的版本用 override 修飾符標記。

Bicycleinit() 初始化器首先調用 super.init(),它調用 Bicycle 的 superclass Vehicle 的默認初始化器。這確保在 Bicycle 有機會修改屬性之前,由 Vehicle 初始化 numberOfWheels 繼承的屬性。調用 super.init() 後,numberOfWheels 的原始值將替換成新的值 2

如果創建 Bicycle 的實例,則可以調用其繼承的 description 計算屬性來查看 numberOfWheels 屬性的更新方式:

如果 subclass 初始化器在初始化過程的階段2中不執行自定義,並且 superclass 具有零參數的指定初始化器,則可以在將值分配給所有 subclass 的存儲屬性後省略對 super.init() 的調用。

這個範例定義了 Vehicle 的另一個 subclass,稱為 Hoverboard。在初始化器中,Hoverboard 僅設置其顏色屬性。在初始化器不依賴於 super.init() 的顯示調用,而是依賴其 superclass 的初始化的隱式調用來完成該過程。

Hoverboard 的實例使用 Vehicle 的初始化器提供的默認 wheels 數量。

NOTE
Subclasses 可以在初始化期間修改繼承的變數屬性,但不能修改繼承的常數屬性。

# 自動初始化器繼承

如上所述,默認情況下 subclass 不會繼承其 superclass 初始化器。但是,如果滿足某些條件,則會自動繼承 superclass 初始化器。實際上,這意味著你不需要在許多常見的場景中編寫初始化器的重寫,並且可以在最小的功夫繼承你的 superclass 的初始化器。

假設你為在 subclass 中引入的任何新屬性提供默認值,則是用以下兩個規則:

  • 規則1
    如果你的 subclass 沒有任何指定初始化器,它會自動繼承其所有 superclass 的指定初始化器。
  • 規則2
    如果你的 subclass 提供了超過所有 superclass 指定初始化器的實現,透過按照規則1繼承他們,或者透過提供自定義實現作為其定義的一部分,那麼他會自動繼承所有 superclass 便捷初始化器。

即使你的 subclass 添加了更多便捷初始化器,這些規則也適用。

Subclass 可以將 superclass 的指定初始化器實現作為便捷初始化器,作為滿足規則2的一部分。

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

以下範例表示了指定初始化器、便捷初始化器和自動初始化器繼承。這個範例定義了名為 FoodRecipeIngredientShoppingListItem 的三個 class 的層次結構,並且顯示了他們的初始化器如何相互作用的。

在這個層次階層中的 base clasee 稱為 Food,它是一個封裝食品名稱的簡單 classFood 引入了一個名為 nameString 屬性,並且提供了兩個用於創建 Food 實例的初始化器。

下圖表示了 Food class 的初始化器鏈:

Classes 沒有默認的成員初始化器,因此 Food 提供了一個指定初始化器,它接受一個名為 name 的參數。這個初始化器可用於創建沒有特定名稱的 Food 實例。

Food 中的 init(name: String) 初始化器作為指定初始化器提供,因為它確保完全初始化新 Food 實例的所有存儲屬性。Food 沒有 superclass,因此 init(name: String) 初始化器不需要調用 super.init() 來完成初始化。

Food 還提供了一個沒有參數的便捷初始化器 init() ,初始化器透過委託給名為 [Unnamed] 的名稱值 init(name: String) 來為新食物提供默認的佔位符名稱。

層次階層中的第二個 class 為 Food 的 subclass,稱為 RecipeIngredient。它引入了一個名為 quantityInt 屬性( 除了它繼承自 Food 之外的 name 屬性 ),並且定義了兩個用於創建 RecipeIngredient 實例的初始化器。

下圖表示了 RecipeIngredient 的初始化器鏈:

RecipeIngredient 有一個指定初始化器 init(name: String, quantity: Int),可用於填充 RecipeIngredient 實例的所有屬性。這個初始化器首先將傳遞的數量參數分配給 quantity,這是 RecipeIngredient 引入的唯一新屬性。執行此操作後,初始化器將向上委託給 Foodinit(name: String) 初始化器。這個過程滿足兩段式初始化的安全檢查1。

RecipeIngredient 還定義了一個便捷初始化器 init(name: String),它僅用於按照名稱創建 RecipeIngredient 實例。對於在沒有顯式數量的情況下創建任何 RecipeIngredient 實例,此便捷初始化器假定數量為 1。此便捷初始化器的定義使 RecipeIngredient 實例更快更方便地被創建,並且創建多個單個數量的 RecipeIngredient 實例時避免程式碼重複。這個便捷初始化只需委託給 class 的初始化器,傳遞 quantity1

RecipeIngredient 提供的 init(name: String) 便捷初始化器採用 Food 中的 init(name: String) 指定初始化器相同的參數。由於此便捷初始化器會覆寫其 superclass 的指定初始化器,因此必須使用 override 修飾符進行標記。

儘管 RecipeIngredient 提供了 init(name: String) 初始化器作為便捷初始化器,但 RecipeIngredient 仍提供了其所有 superclass 指定初始化器的實現。因此,RecipeIngredient 也會自動繼承其所有 superclass 的便捷初始化器。

在這個範例中,RecipeIngredient 的 superclass 是 Food,它有一個名為 init() 便捷初始化器。因此,這此初始化器由 RecipeIngredient 繼承。init() 的繼承版本與 Food 版本完全相同,只是它委託給 RecipeIngredient 版本的 init(name: String) 而不是 Food 版本。

這三個初始化器都可以用於創建新的 RecipeIngredient 實例。

層次階層中的第三個和最後一個 class 是 RecipeIngredient 的一個 subclass 稱為 ShoppingListItemShoppingListItem 對購物清單顯示的配方成份進行建模。

購物清單中的每件商品都以 “未購買” 為起始。為了表示這一個事實,ShoppingListItem 引入了一個名為 purchasedBool 屬性,默認值為 falseShoppingListItem 還添加了一個計算屬性 description,該屬性提供了 ShoppingListItem 實例的文本描述。

NOTE
ShoppingListItem
沒有定義初始化器來為 purchased 提供初始值,因為購物清單的項目總是起始於未購買。

因為它引入的所有屬性提供了默認值,並且沒有自己定義任何初始化器,所以 ShoppingListItem 會自動從其 superclass 繼承所有指定和便捷初始化器。

下圖演示了三個 class 整體的初始化器鏈:

你可以使用所有三個繼承的初始化器來創建新的 ShoppingListItem 實例:

這邊我們有一個新的 array 稱為 breakfastList ,其包含三個新的 ShoppingListItem 的實例,推斷其類型為 [ShoppingListItem]

創建 array 後,Array 中的第一個 ShoppingListItem 的名稱從 [Unnamed] 更改為 Orange juice,並且標記已購買。

接著印出 breakfastList 中的每個項目來表明他的的默認狀態已經按照預期被設置。

# 後記

希望這篇文章能夠讓大家對於指定初始化器、便捷初始化器和自動初始化器繼承有一定的了解,並且了解到如何去交互使用它們,並且在適當的時機使用或是 override,下一篇文章依然會講到可失敗初始化器,以及額外的一些項目。

# 參考文件:

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]