Swift-構造過程

優質神經病發表於2018-08-03

前言

構造過程是使用類、結構體或列舉型別的例項之前的準備過程。在新例項可用前必須執行這個過程,具體操作包括設定例項中每個儲存屬性的初始值和執行其他必須的設定或初始化工作。

通過定義構造器來實現構造過程,就像用來建立特定型別新例項的特殊方式。與OC中的構造器不同,Swift的構造器無需返回值,它們的主要任務是保證新實在第一次使用前完成正確的初始化。

類的例項也可以通過定義析構器在例項釋放之前執行特定的清楚工作。

本文包括:儲存屬性的初始賦值自定義構造過程預設構造器值型別的構造器代理類的繼承和構造過程可失敗構造器必要構造器通過閉包或函式社會組屬性的預設值

儲存屬性的初始賦值

類和結構體在建立例項時,必須為所有儲存型屬性設定合適的初始值。儲存型屬性的值不能處於一個未知的狀態。

可以在構造器中為儲存型屬性賦初始值,也可以在定義屬性時為其設定預設值。

注: 當未儲存型屬性設定預設值或者在構造器中為其賦值時,它們的值是被直接設定的,不會觸發任何屬性觀察者。

構造器

構造器在建立某個特定型別的新例項時被呼叫。它的最簡形式類似於一個不帶任何引數的例項方法,以關鍵字init命名:

init() {
    // 在此處執行構造過程
}
複製程式碼

下面例子中定義了一個用來儲存華氏溫度的結構體Fahrenheit,它擁有一個Double型別的儲存型temperature

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// 列印 "The default temperature is 32.0° Fahrenheit"
複製程式碼

這個結構體定義了一個不帶引數的構造體init,並在裡面將儲存型屬性temperature的值初始化為32.0

預設屬性值

如前所述,可以在構造器中為儲存型屬性設定初始值。同樣也可以在屬性宣告時為其設定預設值。

注: 如果一個屬性總是使用相同的初始值,那麼為其設定一個預設值比每次都在構造器中賦值要好。兩種方法的效果是一樣的,只不過使用預設值讓屬性的初始化和宣告結合得更緊密。使用預設值能讓你的構造器更簡潔、更清晰,且能通過預設值自動推匯出屬性的型別;同時,它也能讓你充分利用預設構造器、構造器繼承等特性。

可以使用更簡單的方式在定義結構體Fahrenheit時為屬性temperature設定預設值:

struct Fahrenheit {
    var temperature = 32.0
}
複製程式碼

自定義構造過程

可以通過輸入引數和可選型別的屬性來自定義構造過程,也可以在構造中給常量屬性賦初始值。

構造引數

自定義構造過程時,可以在定義中提供構造引數,指定引數值的型別和名字。構造引數的功能和語法跟函式和方法相同。

下面例子中定義了一個包含攝氏度溫度的結構體Celsius。它定義了兩個不同的構造器:init(fromFahrenheit:)init(fromKevin:),二者分別通過接受不同溫標下的溫度值來建立新的例項:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius 是 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius 是 0.0
複製程式碼

第一個構造器擁有一個構造引數,其外部名字為fromFahrenheit,內部名字為Fahrenheit;第二個構造器也擁有一個構造引數,其外部名字為fromKelvin,內部名字為Kelvin。這兩個構造器都將唯一的引數值轉換成攝氏溫度值,並儲存在屬性temperatureInCelsius中。

引數名和引數標籤

跟函式和方法引數相同,構造引數也擁有一個在構造器內部使用的引數名和一個在呼叫構造器時使用的引數標籤。

然而,構造器並不像函式和方法那樣在括號前有一個可辨別的名字。因此在呼叫構造器時,主要通過構造器中的引數名和型別來確定應該被呼叫的構造器。正因為引數如此重要,如果在定義構造器時沒有提供引數標籤,Swift將會為構造器每個引數自動生成一個引數標籤。

以下例子中定義了一個結構體Color,它包含了三個常量:redgreenblue。這些屬性可以儲存0.01.0之間的值,用來指示顏色中紅、綠、藍成分的含量。

Color提供了一個構造器,其中包含三個Double型別的構造引數。Color也提供了第二個構造器,它只包含名為whiteDouble型別的引數,它被用於給上述三個構造引數賦予統同樣的值。

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}
複製程式碼

兩種構造器都能通過提供的初始引數值來建立一個新的 Color 例項:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)
複製程式碼

注: 如果不通過引數標籤傳值,是沒法呼叫這個構造器的。只要構造器定義了某個引數標籤,就必須使用它,忽略它將導致編譯錯誤。

不帶引數標籤的構造器引數

如果不希望為構造器的某個引數提供引數標籤,可以使用下劃線(_)來顯示描述它的外部名,以此重寫上面所說的預設行為。

下面是之前Celsius例子的擴充套件,跟之前相比新增了一個帶有Double型別引數的構造器,其外部名用_代替:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double){
        temperatureInCelsius = celsius
    }
}

let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius 為 37.0
複製程式碼

呼叫 Celsius(37.0) 意圖明確,不需要引數標籤。因此適合使用 init(_ celsius: Double) 這樣的構造器,從而可以通過提供未命名的 Double 值呼叫構造器,而不需要加上引數標籤。

可選屬性型別

如果定製的型別包含一個邏輯上允許取值為空的儲存屬性--無論是因為它無法在初始化賦值,還是因為它在之後某個時間點可以賦值為空--都西藥將它定義為可選型別。可選型別的屬性將自動初始化nil,表示這個屬性是有意在初始化時設定為空的。

下面例子中定義了類SurveyQuestion,它包含一個可選字串屬性response

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}

let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// 列印 "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."
複製程式碼

調查問題的答案在回答前是無法確定的,因此我們將屬性response宣告為String?型別,或者說是可選字串型別。當SurveyQuestion例項化時,它將自動賦值為nil,表明此字串暫時還沒有值,

構造過程中常量屬性的賦值

可以在構造過程中的任意時間點給常量屬性指定一個值,只要在構造過程結束時是一個確定的值。一旦常量屬性被賦值,它將永遠不可更改。

注: 對於類的例項來說,它的常量屬性只能在定義它的類構造過程中修改,不能在子類中修改。

可以修改上面的SurveyQuestion示例,用常量屬性替代變數屬性text,表示問題內容textSurveyQuestion的例項被建立之後不會再被修改。儘管text屬性現在是常量,我們仍然可以在類的構造器中設定它的值:

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// 列印 "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
複製程式碼

預設構造器

如果結構體或類的所有屬性都有預設值,同時沒有自定義的構造器,那麼Swift會給這些結構體或類提供一個預設構造器。這個預設構造器將簡單的建立一個所有屬性都設定為預設值的例項。

下面例子中建立了一個類ShoppingListItem,它封裝了購物清單中的某一物品的屬性:名字(name)、數量(quantity)和購買狀態(purchase state):

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()
複製程式碼

由於 ShoppingListItem 類中的所有屬性都有預設值,且它是沒有父類的基類,它將自動獲取一個可以為所有屬性設定預設值的預設構造器(儘管程式碼中沒有顯式為name屬性設定預設值,但由於name是可選字串,它將預設設定為nil)。上面例子中使用預設構造器創造了一個 ShoppingListItem 類的例項(使用 ShoppingListItem() 形式的構造器語法),並將其賦值給變數 item

結構體的逐一成員構造器

除了上面提到的預設構造器,如果結構體沒有提供自定義的構造體,它們將自動獲得一個逐一成員構造器,即使結構體的儲存型屬性沒有預設值。

逐一成員構造器時用來初始化結構體新例項裡成員屬性的快捷方法。我們在呼叫逐一成員構造器時,通過與成員屬性名相同的引數名進行傳值類完成對成員屬性的初始賦值。

下面的例子中定義了一個結構體Size,它包含兩個屬性widthheight。Swift可以根據這兩個屬性的初始賦值0.0自動推斷出它們的型別是Double

結構體Size自動獲得一個逐一成員構造器init(width:height:)。可以用它來建立新的Size例項:

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
複製程式碼

值型別的構造器代理

構造器可以通過呼叫其他構造器來完成例項的部分構造過程。這一過程稱為構造器代理,它能避免多個構造器的程式碼重複。

構造器代理的實現規則和形式在值型別和類型別中有所不同。值型別(結構體和列舉型別)不支援繼承,所以構造器代理的過程相對簡單,因為它們只能代理給自己的其他構造器。類則不同,它可以繼承自其它類,這意味著類有責任保證其他所以繼承的儲存型屬性在構造時也能正確的初始化。

對於值型別,可以使用self.init在自定義的構造器中引用相同型別中的其他構造器。並且只能在構造器內部呼叫self.init

注: 如果為了某個值型別定義了一個自定義的構造器,將無法訪問到預設構造器(如果是結構體,還將無法訪問逐一成員構造器)。這種限制可以防止你為值型別增加了一個額外的且十分複雜的構造器之後,仍然會錯誤的使用自動生成的構造器。

下面的例子將定義一個結構體Rect,用來代表幾何矩形。這個例子需要兩個輔助的結構體SizePoint,它們各自為其所以的屬性提供了預設初始值0.0

struct Size {
    var width = 0.0, height = 0.0
}

struct Point {
    var x = 0.0, y = 0.0
}
複製程式碼

可以通過以下三種方式為Rect建立例項--使用含有預設值的originsize屬性來初始化;提供指定的originsize例項來初始化;提供指定的centersize來初始化。在下面Rect結構體定義中,我們用三種方式提供了三個自定義的構造器:

struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}
複製程式碼

第一個Rect構造器init(),在功能上跟沒有自定義構造器時自動獲得的構造器是一樣的。這個構造器是一個空函式,使用一對大括號{}來表示。呼叫這個構造器將返回一個Rect例項,它的originsize屬性都使用定義的預設值Point(x:0.0, y: 0.0)Size(width: 0.0, height: 0.0)

let basicRect = Rect()
// basicRect 的 origin 是 (0.0, 0.0),size 是 (0.0, 0.0)
複製程式碼

第二個 Rect 構造器 init(origin:size:),在功能上跟結構體在沒有自定義構造器時獲得的逐一成員構造器是一樣的。這個構造器只是簡單地將 originsize 的引數值賦給對應的儲存型屬性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
    size: Size(width: 5.0, height: 5.0))
// originRect 的 origin 是 (2.0, 2.0),size 是 (5.0, 5.0)
複製程式碼

第三個 Rect 構造器 init(center:size:) 稍微複雜一點。它先通過 centersize 的值計算出 origin 的座標,然後再呼叫(或者說代理給)init(origin:size:) 構造器來將新的 originsize 值賦值到對應的屬性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
    size: Size(width: 3.0, height: 3.0))
// centerRect 的 origin 是 (2.5, 2.5),size 是 (3.0, 3.0)
複製程式碼

構造器 init(center:size:) 可以直接將 originsize 的新值賦值到對應的屬性中。然而,構造器init(center:size:)通過使用提供了相關功能的實現有構造器將會更加便捷。

類的繼承和構造過程

類裡面的所有儲存型屬性--包括所有繼承自父類的屬性--都必須在構造過程中設定初始值。

Swift為類型別提供了兩種構造器來確保例項中所有儲存型屬性都能獲得初始值,它們分別是指定構造器和便利構造器。

指定構造器和便利構造器

指定構造器是類中最主要的構造器。一個指定構造器將初始化類中提供的所有屬性,並根據父類鏈往上呼叫父類合適的構造器來實現父類的初始化。

類傾向於擁有少量指定構造器,普遍的是一個類擁有一個指定構造器。指定構造器在初始化的地方通過管道將初始化過程持續到父類鏈。

每一個類都必須至少擁有一個指定構造器。在某些情況下,許多類通過繼承了父類中的指定構造器而滿足了這個條件。

便利構造器是類中比較次要、輔助型的構造器。你可以定義便利器來呼叫同一個類中的指定構造器,併為其引數提供預設值。也可以定義便利構造器來建立一個特殊用途或特定輸入值的例項。

你應當只在必要的時候為類提供便利構造器,比方說某種情況下通過使用便利構造器來快捷呼叫某個指定構造器,能夠節省更多開發時間並讓類的構造過程更加清晰明瞭。

指定構造器和便利構造器的語法

類的指定構造器的寫法跟值的寫法跟值型別簡單構造器一樣:

init(parameters) {
    statements
}
複製程式碼

便利構造器也採用相同樣式的寫法,但需要在init關鍵字之前放置convenience關鍵字,並使用空格將它們倆分開:

convenience init(parameters) {
    statements
}
複製程式碼

類的構造器代理規則

為了簡化指定構造器和便利構造器之間的呼叫關係,Swift採用了以下三條規則來限制構造器之間的代理呼叫:

  1. 指定構造器必須呼叫其直接父類的指定構造器。
  2. 便利構造器必須呼叫類中定義的其他構造器。
  3. 便利構造器最後必須呼叫指定構造器

一個方便記憶的方法是:

  • 指定構造器必須總是向上代理
  • 便利構造器必須總是橫向代理

構造器代理圖

如圖所示,父類中包含一個指定構造器和兩個便利構造器。其中一個便利構造器呼叫另一個便利構造器,而後者又呼叫了唯一的指定構造器。這滿足了上面提到的規則2和3。這個父類沒有自己的父類,所有規則1沒有用到。

子類中包含兩個指定構造器和一個便利構造器。便利構造器必須呼叫指定構造器的任意一個,因為它只能呼叫一個類裡的其他構造器。這滿足了上面提到的規則2和3.而兩個指定構造器必須呼叫父類中唯一的指定構造器,這滿足了規則1。

注: 這些規則不會影響類的實力如何建立。任何上圖中展示的構造器都可以用來建立完全初始化的例項。這些規則隻影響類的構造器如何實現。

下面的圖例中展示了一種涉及四個類的更負責的嘞層級結構。它演示了指定構造器時如何在類層級中充當管道的作用,在類的構造器鏈上 簡化了類之間的相互關係。

複雜構造器代理圖

兩段式構造過程

Swift中類的構造過程包含兩個階段。第一個階段,類中的每個儲存型屬性賦值一個初始值。當每個儲存型屬性的初始值被賦值後,第二個階段開始,它給每個類一次機會,在新例項準備使用之前進一步定製它們的儲存型屬性。

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

注: Swift的兩段式構造過程跟OC中的構造過程類似,最主要的區別在階段1,OC給每一個屬性賦值0或控制(比如說0nil)。Swift的構造流程則更加靈活,它允許你設定定製的初始值,並自如應對某些屬性不能以0nil作為合法預設值的情況。

Swift編譯器將執行4種有效的安全檢查,以確保兩段式構造過程不出錯的完成:

  1. 安全檢查1--指定構造器必須保證它所在類的所有屬性都必須先初始化完成,之後才能將其它構造任務向上代理給父類中的構造器。一個物件的記憶體只有在其所有儲存型屬性確定之後才能完全初始化。為了滿足這一規則,指定構造器必須保證它所在類的屬性在它往上代理之前先完成初始化。
  2. 安全檢查2--指定構造器必須在為繼承的屬性設定新值之前向上代理呼叫父類構造器,如果沒這麼做,指定構造器賦予的新值將被父類中的構造器所覆蓋。
  3. 安全檢查3--便利構造器必須為任意型別(包括同類中定義的)賦新值之前代理呼叫同一類中的其他構造器,如果沒這麼做,便利構造器賦予的新值將被同一類中其他指定構造器所覆蓋。
  4. 安全檢查4--構造器在第一階段造完之前,不能呼叫任何勢力方法,不能讀取任何例項屬性的值,不能引用self作為一個值。

類例項在第一階段結束以前並不是完全有效的。只有第一階段完成後,該例項才會成為有效例項,才能訪問屬性和呼叫方法。

兩段式構造過程中基於上述安全檢查的構造流程展示:

階段1

  • 某個指定構造器或便利構造器被呼叫
  • 完成新例項記憶體的分配,但此時記憶體還沒有被初始化
  • 指定構造器確保其所在類引入的所有儲存型屬性都已賦初始值。儲存型屬性所屬的記憶體完成初始化
  • 指定構造器將呼叫父類的構造器,完成父類屬性的初始化
  • 這個呼叫父類的構造器的過程沿著構造器鏈一直往上執行,直到到達構造器鏈的最頂部
  • 當到達了構造器鏈最頂部,且已確保所有例項包含的儲存型屬性都已經賦值,這個例項的記憶體被認為已經初始化。此時階段1完成

階段2

  • 從頂部構造器一直往下,每個構造器鏈中類的指定構造器都有機會進一步定製例項。構造器此時可以訪問self,修改它的屬性並呼叫例項方法等等
  • 最終,任意構造器鏈中的便利構造器可以有機會定製例項和使用self

構建過程階段1

這個例子中,構造過程從對子類中一個便利構造器的呼叫開始。這個便利構造器此時沒法修飾任何屬性,它把構造任務代理給同一類中的指定構造器。

如安全檢查1所示,指定構造器將確保所有子類的屬性都有值。然後它將呼叫父類的指定構造器,並沿著構造器鏈一直往上完成父類的構造過程。

父類中的指定構造器確保所有父類的屬性都有值。由於沒有更多的父類需要初始化,也就無需繼續向上代理。

一旦父類中所有屬性都有了初始值,例項的記憶體被認為是完全初始化,階段1完成。

以下展示了相同構造過程的階段2:

構建過程階段2

父類中的指定構造器現在有機會進一步來定製例項(儘管這不是必須的)。 一旦父類中的指定構造器完成呼叫,子類中的指定構造器可以執行更多的定製操作。

構造器的繼承和重寫

跟OC中的子類不同,Swift中的子類預設情況下不會繼承父類的構造器。Swift的這種機制可以防止一個父類簡單構造器被一個更精細的子類繼承,並被錯誤的用來建立子類的例項。

注: 父類的構造器僅會在安全和適當的情況下被繼承。

假如嚮往自定義的子類中能提供一個或多個跟父類相同的構造器,可以在子類中提供這些構造器的自定義實現。

當在編寫一個父類中指定構造器相匹配的子類構造器時,你實際上實在重寫父類的這個指定構造器。因此,你必須在定義子類構造器時帶上override修飾符。即使你重新的是系統自動提供的預設構造器,也需要帶上override修飾符。

正如重寫屬性,方法或者下標。override修飾符會讓編譯器去檢查父類中是否有匹配的指定構造器,並驗證構造器引數是否正確。

注: 當重寫一個父類的指定構造器時,你總是需要寫override修飾符,即使是為了實現子類的遍歷構造器。

相反,如果編寫了一個父類遍歷構造器相匹配的子類構造器,由於子類不能直接呼叫父類的遍歷構造器,因此,嚴格意義上來講,子類並未對一個父類構造器提供重寫。最後的結果就是,你在子類中重寫一個父類便利構造器時,不需要加override修飾符。

在下面的例子中定義了一個叫Vehicle的基類。基類中宣告瞭一個儲存型屬性numberOfWheels,它是預設值為0Int型別的儲存型屬性。numberOfWheels屬性用於建立名為descriptionString型別的計算屬性:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}
複製程式碼

Vehicle類只為儲存型屬性提供預設值,也沒有提供自定義構造器。因此,它會自動獲得一個預設構造器。自動獲得的預設構造器總是類中指定構造器,它可用於建立numberOfWheels0Vehicle例項:

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)
複製程式碼

下面例子中定義了一個Vehicle的子類Bicycle

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}
複製程式碼

子類Bicycle定義了一個自定義指定構造器init()。這個指定構造器和父類的指定構造器相匹配,所以Bicycle中指定構造器需要帶上override修飾符。

Bicycle的構造器init()以呼叫super.init()方法開始,這個方法的作用是呼叫Bicycle的父類預設構造器。這樣確保Bicycle在修改屬性之前,它所繼承的屬性numberOfWheels能被Vehicle類例項化。在呼叫super.init()之後,屬性numberOfWheels的原始值被新值2替換。

如果建立一個Bicycle例項,可以呼叫繼承的description計算型屬性去檢視屬性numberOfWheels是否有改變:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// 列印 "Bicycle: 2 wheel(s)"
複製程式碼

注: 子類可以愛初始化是修改繼承來的變數屬性,但是不能修改繼承來的常量屬性。

構造器的自動繼承

如上所述,子類在預設荊軻下不會繼承父類的構造器。但是如果滿足特定條件,父類構造器是可以被自定繼承的。事實上,這意味著對於許多常見場景你不必重新父類的構造器,並且可以在安全的情況下以最少的程式碼繼承父類的構造器。

假設為子類中引入的所有新屬性都提供了預設值,以下2個規則適用:

規則1--如果子類沒有定義任何指定的構造器,它將自動繼承父類所有的指定構造器 規則2-- 如果子類提供了所有父類指定構造器的實現,無論是通過規則1繼承過來的,還是提供了自定義實現,它將自動繼承父類所有的遍歷構造器

即使你在子類中新增了更多的便利構造器,這兩條規則仍然適用。

**注:**對於規則2,子類可以將父類的指定構造器實現為便利構造器。

指定構造器和便利構造器實踐

接下來的例子將在實踐中展示指定構造器、便利構造器以及構造器的自動繼承。這個例子定義了包含三個類FoodRecipeIngredient以及ShoppingListItem的類層次結構,並將演示他們的構造器是如何相互作用的。

類層次中的基類是Food,它是一個簡單的用來封裝食物名字的類。Food類引入了一個叫做nameString型別的屬性,並且提供了兩個構造器來建立Food例項:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}
複製程式碼

下圖展示了Food的構造器鏈:

Food構造器鏈

類型別沒有預設的逐一成員構造器,所有Food類提供了一個接受單一引數name的指定構造器。這個構造器可以使用一個特定的名字來建立新的Food例項:

let namedMeat = Food(name: "Bacon")
// namedMeat 的名字是 "Bacon"
複製程式碼

Food類中的構造器init(name:String)被定義為一個指定構造器,因為它能確保Food例項的所有儲存型屬性都被初始化。Food類沒有父類,所以init(name: String)並給引數name賦值為[Unnamed]來實現:

let mysteryMeat = Food()
// mysteryMeat 的名字是 [Unnamed]
複製程式碼

類層級中的第二個類是Food的子類RecipeIngredientRecipeIngredient類用來表示食譜中的一項原料。它引入了Int型別的屬性quantity(以及從Food繼承過來的name屬性),並且定義了兩個構造器來建立RecipeIngredient例項:

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)
    }
}
複製程式碼

下圖中展示了 RecipeIngredient 類的構造器鏈:

RecipeIngredient 構造器

RecipeIngredient類擁有一個指定構造器init(name:String, quantity: Int),它可以用來填充RecipeIngredient例項的所有屬性值。這個構造器一開始先將傳入的quantity引數賦值給quantity屬性,這個屬性也是唯一在RecipeIngredient中引入的屬性。隨後,構建器向上代理到父類Foodinit(name:String)。這個過程滿足中安全檢查。

RecipeIngredient也定義了一個便利構造器init(name:String),它只通過name來建立RecipeIngredient的例項。這個便利構造器假設任意RecipeIngredient例項的quantity1,所以不需要顯式指明數量即可建立出例項,這個便利構造器的定義可以更加方便和快捷的建立例項,並且避免建立多個quantity1RecipeIngredient例項時的程式碼重複。這個便利構造器只是簡單地橫向代理到類中的指定構造器,併為quantity引數傳遞1

注: RecipeIngredient的遍歷構造器init(name:String)使用了跟Food中指定構造器init(name:String)相同的引數。由於這個便利構造器重寫了父類的指定構造器init(name:String),因此必須在前面使用override修飾符。

儘管RecipeIngredient將父類的指定構造器重寫為了便利構造器,但是它依然提供了父類的所有指定構造器的實現。因此,RecipeIngredient會自動繼承父類的所有便利構造器。

在這個例子中,RecipeIngredient的父類是Food,它有一個便利構造器init(),這個便利構造器會被RecipeIngredient繼承。這個繼承版本init()在功能上跟Food提供的版本是一樣的。

所有的這個三種構造器都可以用來建立新的RecipeIngredient例項:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
複製程式碼

類層級中第三個也是最後一個類是RecipeIngredient的子類,叫做ShoppingListItem。這個類構建了購物單中出現的某一種食譜原料。

購物單中的每一項總是從未購買狀態開始的。為了呈現這一事實,shippingListItem引入了一個布林型別purchased,它的預設值是falseShoppingListItem還新增了一個計算型屬性description,他提供了關於ShoppingListItem例項的一些文字描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}
複製程式碼

注: ShoppingListItem 沒有定義構造器來為 purchased 提供初始值,因為新增到購物單的物品的初始狀態總是未購買。

由於它為自己引入的所有屬性都提供了預設值,並且自己沒有定義任何構造器,ShoppingListItem 將自動繼承所有父類中的指定構造器和便利構造器。

三類構造器圖

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x orange juice ✔
// 1 x bacon ✘
// 6 x eggs ✘
複製程式碼

如上所述,例子中通過字面量方式建立了一個陣列breakfastList,它包含了三個ShoppingListItem例項,因此陣列的型別也能被自動推導為[ShoppingListItem]。在陣列建立完之後,陣列中第一個ShoppingListItem例項名字從[Unnamed]更改為Orange juice,並標記狀態為已購買,列印陣列中每個元素的描述顯示了它們都已按照預期被賦值。

可失敗構造器

如果一個類、結構體或列舉型別的物件,在構造過程中有可能失敗,則為其定義一個可失敗構造器時很有用的。這裡所指的失效指的是,如給構造器傳入無效的引數值,或缺少某種所需的外部資源,又或是不滿足某種必要的條件的等。

注: 可失敗構造器的引數和引數型別,不能與其他非可失敗構造器的引數名,及其引數型別相同。

可失敗的構造器或建立一個型別為自身型別的可選型別物件。通過return nil語句來表明可失敗構造器在何種情況下應該失敗

嚴格來說,構造器都不支援返回值。因為構造器本身的作用,只是為了確保物件能被正確構造。因此只是用return nil表明失敗構造器構造失敗。而不要用關鍵字return來表明構造成功。

例如,實現針對數字型別轉換的可失敗構造器。確保數字型別之間的轉換能儲存精確的值,使用這個init(exactly:)構造器。如果型別轉換不能保持值不變,則這個構造器構造失敗。

let wholeNumber: Double = 12345.0
let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {
    print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
}
// 列印 "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged 是 Int? 型別,不是 Int 型別

if valueChanged == nil {
    print("\(pi) conversion to Int does not maintain value")
}
// 列印 "3.14159 conversion to Int does not maintain value"
複製程式碼

下例中,定義了一個名為Animal的結構體,其中有一個名為speciesString型別的常量屬性。同時該結構體還定義了一個接受一個名為speciesString型別引數的可失敗構造器。這個可失敗構造器檢查傳入引數是否為一個空字串。如果為空字串,則構造失敗。否則,species屬性被賦值,構造成功。

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty {
        	return nil
        }
        self.species = species
    }
}
複製程式碼

可以通過該失敗構造器來嘗試構建一個Animal的例項,並檢查構造過程是否成功:

let someCreature = Animal(species: "Giraffe")
// someCreature 的型別是 Animal? 而不是 Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// 列印 "An animal was initialized with a species of Giraffe"
複製程式碼

如果給該可失敗構造器傳入一個空字串作為其引數,則會導致構造失敗:

let anonymousCreature = Animal(species: "")
// anonymousCreature 的型別是 Animal?, 而不是 Animal

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// 列印 "The anonymous creature could not be initialized"
複製程式碼

空字串(如"",而不是"Giraffe")和一個值為nil的可選型別的字串是兩個完全不同的概念。上例中的空字串("")其實是一個有效的,非可選型別的字串。這裡我們之所以讓Animal的可失敗構造器構造失敗,只是因為對於Animal這個類的species屬性來說,它更適合有一個具體的值,而不是空字串。

列舉型別的可失敗構造器

可以通過一個帶一個或多個引數的可失敗構造器來獲取列舉型別中特定的列舉成員。如果提供的引數無法匹配任何列舉成員,則構造失敗。

下例中,定義一個名為TemperatureUnit的列舉型別。其中包含了三個可能的列舉成員(KelvinCelsiusFahrenheit),以及一個根據Character值找出所對應的列舉成員的可失敗構造器:

enum TemperatureUnit {
    case Kelvin, Celsius, Fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .Kelvin
        case "C":
            self = .Celsius
        case "F":
            self = .Fahrenheit
        default:
            return nil
        }
    }
}
複製程式碼

可以利用這個可失敗的構造器在三個列舉成員中獲取一個相匹配的列舉成員,當引數的值不能與任何列舉成員相匹配時,則構造器失敗:

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// 列印 "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// 列印 "This is not a defined temperature unit, so initialization failed."
複製程式碼

帶原始值的列舉型別的可失敗構造器

帶原始值的列舉型別會自帶一個可失敗構造器init?(rawValue:),該可失敗構造器有一個名為rawValue的引數,其型別和列舉型別的原始值型別一致。如果該引數的值能夠和某個列舉成員的原始值匹配,則該構造器會構造相應的列舉成員,否則構造失敗。

因此上面的TemperatureUnit的例子可以重寫為:

enum TemperatureUnit: Character {
    case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// 列印 "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// 列印 "This is not a defined temperature unit, so initialization failed."
複製程式碼

構造失敗的傳遞

類,結構體,列舉的可失敗構造器可以橫行代理到同型別中的其他可失敗構造器。類似的,子類的可失敗構造器也能向上代理到父類的可失敗構造器。

無論是向上代理還是橫向代理,如果代理到的其他可失敗構造器觸發構造失敗,整個構造過程將立即終止,接下來的任何構造程式碼不會再被執行。

注: 可失敗構造器也可以代理到其他的非失敗構造器。通過這種方式,可以增加一個可能的失敗狀態到現有的構造過程中。

下面這個例子,定義了一個名為CartItemProduct類的子類。這個類建立了一個線上購物車中的物品的模型,它有一個名為quantity的常量儲存型屬性,並確保該屬性的值至少為1

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,倘若quantity值無效,則立即終止整個構造過程,返回失敗結果。且不再執行餘下程式碼。同樣的,Product的可失敗構造器首先檢查name的值,加入name值為空字串,則構造器立即執行失敗。

如果通過傳入一個非空字串name以及一個值大於等於1的quantity來建立一個CartItem例項,那麼構造方法能夠成功被執行:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// 列印 "Item: sock, quantity: 2"
複製程式碼

倘若你以一個值為 0 的 quantity 來建立一個 CartItem 例項,那麼將導致 CartItem 構造器失敗:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// 列印 "Unable to initialize zero shirts"

複製程式碼

同樣地,如果你嘗試傳入一個值為空字串的 name 來建立一個 CartItem 例項,那麼將導致父類 Product 的構造過程失敗:

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// 列印 "Unable to initialize one unnamed product"
複製程式碼

重寫一個可失敗構造器

如同其他的構造器,可以在子類中重寫父類的可失敗構造器。或者也可以用子類的非可失敗構造器重寫一個父類的可失敗構造器。這使你可以定義一個不會構造失敗的子類,即使父類的構造器允許構造失敗。

注: 當你用子類的非可失敗構造器重寫父類的可失敗構造器時,向上代理到父類的可失敗構造器的唯一方式是對父類的可失敗構造器的返回值進行強行解包。可以用非可失敗構造器重寫可失敗構造器,但是反過來去不行。

下例定義了一個名為Document的類,name屬性的值必須為一個非空字串或nil,但不能是一個空字串:

class Document {
    var name: String?
    // 該構造器建立了一個 name 屬性的值為 nil 的 document 例項
    init() {}
    // 該構造器建立了一個 name 屬性的值為非空字串的 document 例項
    init?(name: String) {
        self.name = name
        if name.isEmpty { return nil }
    }
}
複製程式碼

下面這個例子,定義了一個Document類的子類AutomaticallyNamedDocument。這個子類重寫了父類的兩個指定構造器,確保了無論是使用init()構造器還是init(name:)構造器並未引數傳遞空字串,生成的例項中的name屬性總有初始"[Untitled]"

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}
複製程式碼

AutomaticallyNamedDocument用一個非可失敗構造器init(name:)重寫了父類的可失敗構造init?(name:)。因為子類用另一種方式處理了空字串的情況,所以不再需要一個可失敗構造器,因此子類用一個非可失敗構造器代替了父類的可失敗構造器。

可以在子類的非可失敗構造器中使用強制解包來呼叫父類的可失敗構造器。比如,下面的UNtitle的Document子類的name屬性的值總是"[Untitled]",它在構造過程中使用了父類的可失敗構造器init?(name:)

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}
複製程式碼

在這個例子中,如果在呼叫父類的可失敗構造器init?(name:)時傳入的是空字串,那麼強制解包操作會引發執行時錯誤。不過,因為這裡是通過非空的字串常量來呼叫它,所以並不會發生執行時錯誤。

init!可失敗構造器

通常來說我們通過在init關鍵字後新增問號的方式(init?)來定義一個可失敗構造器,但你也可以通過在init後面新增!的方式來定義一個可失敗構造器(init!),該課失敗構造器將會構建一個對應型別的隱式解包可選型別的物件。

可以在init?中代理到init!,反之亦然。你也可以用init?重新init!,反之亦然。你還可以用init代理到init!,不過,一旦init!構造失敗,則會觸發一個斷言。

必要構造器

在類的構造器前新增required修飾符表明所有該類的子類都必須實現該構造器:

class SomeClass {
    required init() {
        // 構造器的實現程式碼
    }
}
複製程式碼

在子類重寫父類的必要構造器時,必須在子類的構造器前新增required修飾符,表明該構造器要求也應用於繼承鏈後面的子類。在重寫父類中必要的指定構造器時,不需要新增override修飾符:

class SomeSubclass: SomeClass {
    required init() {
        // 構造器的實現程式碼
    }
}
複製程式碼

注: 如果子類繼承的構造器能滿足必要構造器的要求,則無須在子類中顯式提供必要構造器的實現。

通過閉包或函式設定屬性的預設值

如果某個儲存型屬性的預設值需要一些定製或設定,可以使用閉包或全域性函式為其提供預設值。每當某個屬性所在型別的新例項被建立時,對應的閉包或函式會被呼叫,而它們的返回值會當做預設值賦值給這個屬性。

這種型別的閉包或函式通常會建立一個跟屬性型別相同的臨時變數,然後修改它的值以滿足預期的初始狀態,最後返回這個臨時變數,作為屬性的預設值。

下面介紹如何使用閉包為屬性提供預設值的模板

class SomeClass {
    let someProperty: SomeType = {
        // 在這個閉包中給 someProperty 建立一個預設值
        // someValue 必須和 SomeType 型別相同
        return someValue
    }()
}
複製程式碼

閉包結尾的花括號後面接了一對空的小括號。用來告訴Swift立即執行此閉包。如果忽略了這對括號,相當於將閉包本身作為值賦值給了屬性,而不是將閉包的返回值賦值給屬性。

如果使用閉包來初始化屬性,記住在閉包執行是,例項的其他部分都還沒有初始化,這意味著不能再閉包裡訪問其他屬性,即使這些屬性有預設值。同樣,也不能使用隱式的self屬性,或者呼叫任何例項方法。

下面例子中定義了一個結構體 Chessboard,它構建了西洋跳棋遊戲的棋盤,西洋跳棋遊戲在一副黑白格交替的 8 x 8 的棋盤中進行的:

西洋跳棋棋盤

為了呈現這副遊戲棋盤,Chessboard結構體定義了一個屬性boardColors,它是一個包含64Bool值陣列。在陣列中,值為trun的元素表示一個黑格,值為false的元素表示一個白格。陣列中第一個元素代表棋盤上左上角的格子,最後一個元素代表棋盤上右下角的格子。

boardColors陣列是通過一個閉包來初始化並設定顏色值的:

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}
複製程式碼

每當一個新的 Chessboard 例項被建立時,賦值閉包則會被執行,boardColors 的預設值會被計算出來並返回。上面例子中描述的閉包將計算出棋盤中每個格子對應的顏色,並將這些值儲存到一個臨時陣列 temporaryBoard 中,最後在構建完成時將此陣列作為閉包返回值返回。這個返回的陣列會儲存到 boardColors 中,並可以通過工具函式 squareIsBlackAtRow 來查詢:

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false”
複製程式碼

相關文章