類的繼承和構造過程

本頁包含內容:

[TOC]

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

Swift為類類型提供了兩種構造器來確保實例中所有存儲型屬性都能獲得初始值,分別是:

  • 指定構造器
  • 便利構造器

指定構造器和便利構造器

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

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

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

遍歷構造器是類中比較次要的、輔助型的構造器。你可以定義便利構造器來調用一個類中的指定構造器,并為其參數提供默認值。你也可以定義便利構造器來創建一個特殊用途或特定輸入值的實例。

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

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

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

init(parameters) {
    // statements
}

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

convenience init(parameters) {
    // statements
}

類的構造器代理規則

為了簡化指定構造器和便利構造器之間的調用關系,Swift采用了一下三條規則類限制構造器之間的代理調用:

  1. 指定構造器必須調用其直接父類的指定構造器。
  2. 便利構造器必須調用同類中定義的其它構造器。
  3. 便利構造器最后必須調用指定構造器。

一個更方便的記憶方法是:

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

這些規則可以通過下面的圖例來說明:

[圖片上傳失敗...(image-4e3b82-1520508470408)]

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

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

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

[圖片上傳失敗...(image-2ff209-1520508470408)]

兩段式構造過程

Swift中類的構造過程包含兩個階段:

  • 第一個階段,類中的每個存儲型屬性賦一個初始值。當每個存儲型屬性的初始值被賦值后,第二階段開始。
  • 第二階段,它給每個類一次機會,在新實例準備使用之前進一步定制它們的存儲型屬性。

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

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

4種安全檢查

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

  1. 指定構造器必須保證它所在類的所有屬性都必須先初始化完成,之后才能將其它構造任務向上代理給父類中的構造器。一個對象的內存只有在其所有存儲型屬性確定之后才能完全初始化。為了滿足這一規則,指定構造器必須保證它所在類的屬性在它往上代理之前先完成初始化。
  1. 指定構造器必須在為繼承的屬性設置新值之前向上代理調用父類構造器,如果沒有這么做,指定構造器賦予的新值將被父類中的構造器所覆蓋。
  2. 便利構造器必須為任意屬性賦新值之前代理調用同一類中的其他構造器,如果沒有這么做,便利構造器賦予的新值將被同一類中其它指定構造器所覆蓋。
  3. 構造器在第一階段完成之前,不能調用任何實例方法,不能讀取任何實例屬性的值,不能引用self作為一個值。類實例在第一階段結束以前并不是完全有效的。只有在第一階段完成后,該實例才會成為有效實例,才能訪問屬性和調用方法。

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

階段1:

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

階段2:

  • 從頂部構造器鏈一直往下,每個構造器鏈中類的指定構造器都有機會進一步定制實例。構造器此時可以訪問self、修改它的屬性并調用實例方法等等。
  • 最終,任意構造器鏈中的遍歷構造器可以有機會定制實例和使用self

下圖展示了在假定的子類和父類之間的構造階段1:

[圖片上傳失敗...(image-8cb387-1520508470408)]

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

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

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

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

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

[圖片上傳失敗...(image-529f06-1520508470408)]

父類中的指定構造器現在有機會進一步來定制實例(盡管這不是必須的)。

一旦父類中的指定構造器完成調用,子類中的指定構造器可以執行更多的定制操作(這也不是必須的)。

最終,一旦子類的指定構造器完成調用,最開始被調用的遍歷構造器可以執行更多的定制操作。

構造器的繼承和重寫

跟Objective-C中的子類不同,Swift中的子類默認情況下不會繼承父類的構造器。Swift的這種機制可以防止一個父類的簡單構造器被一個更精細的子類繼承,并被錯誤地用來創建子類的實例。

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

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

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

在下面的例子中定義了一個叫 Vehicle 的基類。基類中聲明了一個存儲型屬性 numberOfWheels,它是默認值為 0Int 類型的存儲型屬性。numberOfWheels 屬性用于創建名為 descrpiptionString 類型的計算型屬性:

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的父類Vehicle的默認構造器。這樣可以確保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的構造器鏈:

[圖片上傳失敗...(image-c81f51-1520508470408)]

類類型沒有默認的逐一成員構造器,所以Food類提供了一個接受單一參數name的指定構造器。這個構造器可以使用一個特定的名字來創建新的Food實例:

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

Food類中的構造器init(name: String)被定義為一個指定構造器,因為它能確保Food實例的所有存儲屬性都被初始化。Food類沒有父類,所有init(name: String)構造器不需要調用super.init()來完成構造過程。

Food類同樣提供一個沒有參數的便利構造器init()。這個init()構造器為新食物提供了一個默認的占位名字,通過橫向代理到指定構造器init(name: String)并給name賦值為[Unnamed]來實現:

let mySteryMeat = Food()
// mySteryMeat的名字是"[Unnamed]"

類層級中的第二個類是Food的子類RecipeIngredientRecipeIngredient類用來表示食譜中的一項原料。它引入了Int類型的屬性quantity,并且定義了兩個構造器來創建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類的構造鏈:

[圖片上傳失敗...(image-a5036a-1520508470408)]

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

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版本的init(name: String)而不是Food提供的版本。

所有的三種構造器都可以用來創建新的RecipeIngredient實例:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

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

購物單中的每一項總是從未購買狀態開始的。為了呈現這一事實,ShoppingListItem 引入了一個 Boolean(布爾類型) 的屬性 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將自動繼承所有父類中的指定構造器和便利構造器。

下圖展示了這三個類的構造器鏈:

[圖片上傳失敗...(image-faf8ca-1520508470408)]

你可以使用三個繼承來的構造器來創建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,并標記狀態為已購買。打印數組中每個元素的描述顯示了它們都已按照預期被賦值。

可失敗構造器

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

為了妥善處理這種構造過程中可能會失敗的情況。你可以在一個類,結構體或是枚舉類型的定義中,添加一個或多個可失敗構造器。其語法為在init關鍵字后面添加問號(init?)。

注意:可失敗構造器的參數名和參數類型,不能與其它非可失敗構造器的參數名,及其參數類型相同。

可失敗構造器會創建一個類型為自身類型的可選類型的對象。你通過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類型參數的可失敗構造器。這個可失敗的構造器檢查傳入的參數是否為一個空字符串。如果為空字符串,則構造失敗、否則,sepcied屬性被賦值,構造成功。

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"

枚舉類型的可失敗構造器

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

下例中,定義了一個名為 TemperatureUnit 的枚舉類型。其中包含了三個可能的枚舉成員(KelvinCelsius,和Fahrenheit),以及一個根據 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) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

下面這個例子,定義了一個 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.init()
        } else {
            self.name = name
        }
    }
}

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

你可以在子類的非可失敗構造器中使用強制解包來調用父類的可失敗構造器。比如下面的UntitledDocument子類的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 的棋盤中進行的:

[圖片上傳失敗...(image-5e833d-1520508470408)]

為了呈現這副游戲棋盤,Chessboard結構體定義了一個屬性 boardColors,它是一個包含 64Bool值的數組。在數組中,值為 true 的元素表示一個黑格,值為 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”
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 本章將會介紹 存儲屬性的初始賦值自定義構造過程默認構造器值類型的構造器代理類的繼承和構造過程可失敗構造器必要構造器...
    寒橋閱讀 776評論 0 0
  • 構造過程是使用類、結構體或枚舉類型的實例之前的準備過程。在新實例可用前必須執行這個過程,具體操作包括設置實例中每個...
    莽原奔馬668閱讀 695評論 0 3
  • 構造過程 構造過程是使用類、結構體或枚舉類型的實例之前的準備過程。在新實例可用前必須執行這個過程,具體操作包括設置...
    蠱毒_閱讀 738評論 0 2
  • " 做個入世的俗人,不悲不喜,小心安命,側身修行。" 感恩節值得過分解讀嗎? “我用傳統的方式慶祝感恩節。我要請所...
    法律界的小哥哥閱讀 354評論 0 1
  • 首先去官網下載maven,然后解壓,要配置JAVA_HOME,M2_HOME,最好加上MAVEN_HOME,mav...
    popli閱讀 139評論 0 0