Swift 程式語言 — Initialization (1)

讓我們來了解一下什麼是初始化以及初始化過程。

Jeremy Xue
Jeremy Xue ‘s Blog
13 min readJun 29, 2019

--

Photo by Quino Al on Unsplash

# 前言

初始化( Initialization )是為 Classes、Structures 以及 Enumerations 準備實例的過程,這個過程涉及為該實例上每個儲存屬性設置初始值,並且執行任何所需的其他設置或初始化在新實例準備好使用之前。

你可以透過定義初始化器( Initializers )來實現初始化過程,初始化器類似於調用可以創建特定類型的新實例的特殊方法。與 Objective-C 中的初始化器不同,Swift 中的初始化器不返回值,他們的主要作用是在確保在第一次使用類型的新實例之前正確的被初始化。

Classes 類型的實例有能夠實現反初始化器( Deinitializer ),它在該類型實例被釋放之前執行任何自定義清理,我們在之後 Deinitialization 會再提到。

# 設置儲存屬性的初始值

在創建 Class 或是 Struct 的實例時,Class 和 struct 必須設置所有他們的儲存屬性一個合適的初始值,儲存的屬性不能為不確定的情況。

你可以在初始化器中為儲存屬性設置初始值,或是透過分配默認屬性值作為屬性定義時的一部分處理。

NOTE
當你分配一個默認值給儲存屬性、或是透過初始化器設置初始值時,將直接設置該屬性的值,不會調用到任何屬性觀察者( Property observers )

#初始化器

初始化器在創建特定類型的實例時被調用。在最簡單的形式中,初始化器就像是一個沒有參數的實例方法,使用 init 關鍵字來編寫它:

下方範例我們定義一個名為 Fahrenheitstruct,用來儲存以華氏溫標表達的溫度。Fahrenheit 有一個儲存屬性 temperature 其類型為 Double

struct 定義了單一個沒有參數的初始化器,其將 temperature 初始化為 32.0。我們可以透過下列方式實例化它並且訪問屬性:

# 默認屬性值

如上所示,你可以在初始化器中設置儲存屬性的初始值。另外,指定默認屬性值作為屬性定義的一部份。你指定一個默認屬性值透過在定義時為屬性分配初始化值。

NOTE
如果屬性總是始終使用相同的初始值,請提供默認值,而不是在初始化器中設置值。其最終的結果是相同的,但是默認值將屬性初始化與其宣告更緊密的聯繫在一起。它使初始化器更簡短、更清楚,並使你能夠從默認值去推斷屬性的類型。默認值還使您可以更輕鬆地利用默認初始值器和初始化繼承。

你可以透過在其定義屬性點為其 temperature 屬性提供默認值,以更簡單的形式編寫上面的 Fahrenheit

# 自定義初始化

你可以使用輸入參數和可選屬性類型來自定義初始化過程,或者透過在初始化期間時分配常數屬性,我們會在下面的部分提到。

# 初始化參數

你可以提供初始化參數作為初始化器定義的一部分,來定義自定義初始化過程中值的類型和名稱。初始化參數具有與函數和方法參數相同的功能與語法。

下面範例定義了一個 struct 叫做 Celsius,並且該 struct 儲存了以攝氏度表示的溫度。Celsius 實現了兩個客製化的初始化器,稱為 init(fromFahrenheit:)init(fromKelvin:),它使用不同溫度標準的值來初始化 Celsius 的新實例:

第一個初始化器有一個初始化參數、其參數標籤為 fromFahrenheit、參數名稱為 fahrenheit。第二個初始化器有一個初始化參數,參數標籤為 fromKelvin,參數名稱為 kelvin。兩個初始化器都將參數轉換為相應的 Celsius 值,並將這個值存到 temperatureInCelsius 屬性中。

# 參數名稱和參數標籤

與函數和方法的參數一樣,初始化參數既可以具有在初始化器內部使用的參數名稱,也可以具有在調用初始化器時使用的參數標籤。

但是,初始化器在其括號之前沒有函數和方法的標示函數名稱。因此,初始化器的參數名稱和類型在判斷應該調用哪個初始化器時有著特別重要的作用。所以,如果你不提供初始化器,Swift 會為初始化器中的每個參數提供自動參數標籤。

下面我們定義了一個稱為 Colorstruct,其中包含三個常數屬性,分別為 redgreenblue。這些屬性存儲介於 0.0 到 1.0 之間的值,來表示顏色中紅色、綠色和藍色的數量。

Colorredgreenblue 提供了一個初始化器,其中包含三個 Double 類型的參數。Color 還提供帶有單個 white 參數的第二個初始化器,用於為所有三個顏色提供相同的值。

透過每個初始化參數提供命名值,兩個初始化器都可以被用於創建新的 Color 實例:

注意,如果不使用參數標籤,則無法調用這些初始化器。如果已經定義參數標籤,則必須始終在初始化器中使用參數標籤,如果忽略他們則會發生編譯時錯誤( Compile-time error ):

# 沒有參數標籤的初始化參數

如果你不想要為初始化參數使用參數標籤,請為該參數編寫下劃線 (_) 取代顯式參數標籤來覆蓋默認行為。

這是上面初始化參數的 Celsius 範例的擴展版本,還有一個額外的初始化器,用來從已經處於攝氏度範圍的 Double 值創建新的 Celsius 實例:

調用 Celsius(37.0) 初始化器的意圖很明確,無需參數標籤。因此,將此初始化器編寫為 init(_ celsius: Double) 是適合的,以便可以透過未命名的 Double 值來調用它。

# 可選的屬性類型

如果你自訂的類型邏輯上允許具有 “沒有值” 的存儲屬性 — 可能因為在初始化期間無法設置其值,或者因為在稍後的某個時間點允許它具有 “沒有值” ,宣告這些屬性為可選類型( Optional type )。可選類型的屬性值將自動初始化為 nil,表示該屬性在初始化期間故意具有 “沒有值“ 。

下面範例定義了一個名為 SurveyQuestionclass ,其中包含一個名為 responseOptional String 屬性。

在詢問調查問題之前無法知道對調查問題的回應,因此 response 屬性用 String?Optional String 類型宣告的。當初始化 SurveyQuestion 的新實例時,它會自動分配默認值 nil,意味著 “還沒有字串”。

# 在初始化期間分配常量屬性

只要在初始化完成時將其設置為確定值,就可以在初始化期間的任何時間為常數屬性分配值。為常數屬性分配值後將無法進一步修改。

NOTE
對於 class 實例來說,常數屬性在初始化中只能通過引用的類來修改。它不能被子類修改。

你可以修改上面的 SurveyQuestion 範例,來使用常數屬性的 text ,來表示一旦創建 SurveyQuestion 實例後,問題就不會被修改。即使 text 屬性是常數,他仍然可以在 class 的初始化器中設置:

# 默認初始化器

Swift 為 structclass 提供一個默認初始化器,提供其所有屬性的默認值,並且本身不提供至少一個初始化器。默認的初始化器只是創建一個新實例,其所有屬性都設置為默認值。

下面範例定義了一個名為 ShoppingListItemclass,該個 class 封裝了購物清單中的項目名稱、數量和購買狀態:

因為所有 ShoppingListItem 中的屬性都具有默認值,並且因為它是一個沒有 superclass 的 base class。所以 ShoppingListItem 會自動獲得默認初始化器實現,該實現創建一個新實例,並且將其所有屬性設置為默認值。

上面的範例使用 ShoppingListItem 的默認初始化器以及初始化器語法創建新的實例,寫為 ShoppingListItem(),並將此實例分配給名為 item 的變數。

# Struct 類型的成員初始化器

如果 struct 類型沒有定義任何自定義的初始化器,它們會自動接收成員初始化器( Memberwise initializer )。與默認初始化器不同,即使 struct 存儲的屬性沒有默認值,該 struct 也會接收成員初始化器。

成員初始化器是初始化新 struct 實例的成員屬性的簡便方式。透過名稱將新實例屬性的初始值傳遞給成員初始化器。

下面我們定義了一個名為 Sizestruct,其中包含兩個名為 widthheight 的屬性,透過將其分配默認值 0.0,兩個屬性都被推斷為 Double 類型。

Size 自動接受一個 init(width:height:) 的成員初始化器,你可以使用它初始化一個新的 Size 實例:

當你調用成員初始化器時,你可以審略具有默認值的任何屬性值,在上面的範例中,Size 的 height 和 width 屬性都有一個默認值,你可以省略一個或是兩個屬性,初始化器使用你省略的任何內容的默認值,例如:

⚠️ 注意
這邊在測試時發現其實無法像是 zeroByTwo 的範例一樣省略某個參數,會出現 Cannot invoke initializer for type ‘Size’ with an argument list of type ‘(height: Double)’ 的錯誤訊息,而下面的 zeroByZero 是可用的。
可能官方文檔某些資訊有錯誤。

# Value type 的初始化器委託

初始化器可以調用其他初始化器來執行實例初始化的一部分。這個過程可以稱為初始化器委託( Initializer Delegation ),可以避免跨多個初始化器的重複程式碼。

初始化器委派的工作原理以及允許的委託形式的規則對於 value types 和 class types 是不同的。Value types 不支持繼承,因此它們的初始化器委託過程相對簡單,因為它們只能委託給自己提供的另一個初始化器。但是,Class 可以從其他 class 繼承,如繼承中所述,這意味著 class 具有額外的職責,以確保在初始化期間為其繼承的所有存儲屬性分配合適的值。之後在的 class 繼承和初始化中描述了這些職責。

對於 value types,在編寫自定義的初始化器時,使用 self.init 引用相同 value type 的其他初始化器。你只能在初始化器中調用 self.init

請注意,如果 value types 定義的自訂初始化器,則你將無法再訪問該類型的默認初始化器( 或成員初始化器,如果它是 struct )。這個約束防止了別人意外地使用自動初始化器從而繞過複雜初始化器裡提供的額外必要設置這種情況的發生。

NOTE
如果你想要使用默認的初始化器和成員初始化器以及自定義的初始化器初始化自定義 value type,請再擴展名中編寫自定義初始化器,而不是作為 value types 的原始實現的一部份。

下面範例定義了一個自定義的 Rectstruct 來表示幾何矩形。該範例需要兩個名為 SizePoint 的支持 structures,這兩個 struct 都為其所有的屬性提供默認值 0.0

你可以透過三種方式之一初始化 Rect,使用默認的零初始化 originsize 屬性值,提供特定的原點及大小。這些初始化選項由三個字定義初始化器表示,這些初始化器是 Rect struct 定義的一部份。

第一個 Rect 初始化器 init() ,在功能上與 struct 在沒有自定義的初始化器時將收到的默認值初始化器相同。這個初始化器有一個空的 body,由一對花括弧 {} 表示。調用此初始化器將返回一個 Rect 實例,其 origin 及 size 屬性皆使用默認值 Point(x: 0.0, y: 0.0)Size(width: 0.0, height: 0.0) 從其屬性定義中初始化:

第二個 Rect 初始化器 init(origin:size:),在功能上與結構在沒有自定義的初始化器時會收到的成員初始化器相同。此初始化器只是將 originsize 參數值分配給合適的存儲屬性:

第三個 Rect 初始化器 init(center:size:),他首先根據中心點和尺寸計算適當的原點。然後他調用(委託)init(origin:size:) 初始始化器,他將新的 originsize 存儲在適當的屬性中:

init(center:size:) 初始化器可以將 originsize 的新值分配給適當的屬性本身。但是,init(center:size:) 初始化器利用已經提供了完全相同工能的現有初始化器更為方便( 並且意圖清楚 )。

# 後記:

這次有關初始化的介紹先到這邊,希望大家能夠對怎麼樣透過初始化器初始化有基本的理解,之後還有一些在初始化過程中會用到的更進階技巧,像是指定初始化器、便捷初始化器以及可失敗初始化器等等。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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