14 Initialization 初始化

初始化是準備類、結構或枚舉的實例以供使用的過程。此過程涉及為該實例上的每個存儲屬性設置初始值,并執行新實例準備使用之前需要的任何其他設置或初始化。

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

類類型的實例還可以實現反初始化器deinitializer,它在類的實例被釋放之前執行任何自定義清理。

Setting Initial Values for Stored Properties 設置存儲屬性的初始值

類和結構必須在創建該類或結構的實例時將其所有存儲屬性設置為適當的初始值。存儲的屬性不能處于不確定狀態。

您可以在初始化器中為存儲的屬性設置初始值,或者通過將默認屬性值指定為屬性定義的一部分來實現。下面幾節將描述這些操作。

注意:當為存儲的屬性分配默認值,或在初始化器中設置其初始值時,將直接設置該屬性的值,而不調用任何屬性觀察者。

Initializers 初始化

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

init() {
    // perform some initialization here
}

下面的示例定義了一個名為華氏度的新結構,用于存儲以華氏溫標表示的溫度。華氏結構有一個存儲特性,溫度,類型為Double:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}

var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

該結構定義了一個沒有參數的初始化器init,它初始化存儲的溫度,其值為32.0(水的冰點,以華氏度為單位)。

Default Property Values 默認的屬性值

可以從初始化器中設置存儲屬性的初始值,如上所示。或者,指定一個默認屬性值作為屬性聲明的一部分。通過在定義屬性時為其分配初始值,可以指定默認屬性值。

如果屬性總是使用相同的初始值,則提供默認值,而不是在初始化器中設置值。最終結果是相同的,但是默認值將屬性的初始化與其聲明更緊密地聯系在一起。它使初始化器更短、更清晰,并使您能夠從屬性的默認值推斷屬性的類型。缺省值還使您更容易利用缺省初始化器和初始化器繼承,本章稍后將對此進行描述。

您可以從上面以更簡單的形式編寫華氏結構,方法是在屬性聲明時為其溫度屬性提供一個默認值:

struct Fahrenheit {
    var temperature = 32.0
}

Customizing Initialization 自定義初始化

您可以使用輸入參數和可選屬性類型自定義初始化過程,或者在初始化期間分配常量屬性,如下面的部分所述。

Initialization Parameters 初始化參數

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

下面的示例定義了一個名為攝氏度的結構,它存儲以攝氏度表示的溫度。攝氏度結構實現了兩個自定義初始化器init(from fahrenheit:)和init(from kelvin:),這兩個初始化器使用來自不同溫標的值初始化結構的一個新實例:

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)

let freezingPointOfWater = Celsius(fromKelvin: 273.15)

第一個初始化器有一個初始化參數,參數標簽為fromFahrenheit,參數名稱為fahrenheit。第二個初始化器有一個初始化參數,參數標簽為fromKelvin,參數名稱為kelvin。兩個初始化器都將它們的單個參數轉換為相應的攝氏度值,并將該值存儲在一個名為temperatureInCelsius的屬性中。

Parameter Names and Argument Labels 參數名稱和參數標簽

與函數和方法參數一樣,初始化參數可以在初始化器的主體中使用參數名稱,也可以在調用初始化器時使用參數標簽。

然而,初始化器在圓括號前沒有函數和方法那樣的標識函數名。因此,初始化器參數的名稱和類型在確定應該調用哪個初始化器時起著特別重要的作用。因此,如果不提供初始化器中的每個參數,Swift將為它們提供一個自動參數標簽。

下面的示例定義了一個名為Color的結構,它具有三個常量屬性,分別稱為紅色、綠色和藍色。這些屬性存儲一個0.0到1.0之間的值,表示顏色中紅色、綠色和藍色的數量。

Color為其紅色、綠色和藍色組件提供了一個初始化器,該初始化器具有三個類型為Double的適當命名的參數。Color還提供了第二個初始化器,該初始化器具有一個白色參數,用于為所有三個Color組件提供相同的值。

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)

注意,不使用參數標簽是不可能調用這些初始化器的。如果定義了參數標簽,那么參數標簽必須始終在初始化器中使用,忽略它們編譯時報錯誤:

let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required

Initializer Parameters Without Argument Labels 沒有參數標簽的初始化參數

如果不希望為初始化器參數使用參數標簽,請為該參數編寫下劃線(_),而不是顯式參數標簽,以覆蓋默認行為。

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 is 37.0

初始化器調用攝氏度(37.0)的意圖很清楚,不需要參數標簽。因此,將這個初始化器編寫為init(_ celsius:Double)是合適的,這樣就可以通過提供一個未命名的Double值來調用它。

Optional Property Types 可選的屬性類型

如果您的自定義類型具有邏輯上允許具有“無值”的存儲屬性—可能是因為在初始化期間不能設置其值,或者因為在稍后的某個時刻允許具有“無值”—請使用可選類型聲明該屬性。可選類型的屬性將自動初始化為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()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

調查問題的響應只有在被詢問時才能知道,因此響應屬性是用字符串類型聲明的?,或“可選字符串”。在初始化SurveyQuestion的新實例時,它會自動分配一個默認值nil,意思是“還沒有字符串”。

Assigning Constant Properties During Initialization 在初始化期間分配常量屬性

您可以在初始化期間的任何時刻為常量屬性賦值,只要在初始化完成時將其設置為一個確定的值即可。一旦一個常量屬性被賦值,它就不能被進一步修改。

注意:對于類實例,常量屬性在初始化期間只能由引入它的類修改。它不能被子類修改。

您可以從上面修改SurveyQuestion示例,為問題的文本屬性使用常量屬性而不是變量屬性,以表明一旦創建了SurveyQuestion實例,問題就不會更改。即使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()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

Default Initializers 默認構造器

Swift為任何結構或類提供默認初始化器,這些結構或類為其所有屬性提供默認值,且本身不提供至少一個初始化器。默認初始化器只是創建一個新實例,并將其所有屬性設置為默認值。

這個例子定義了一個名為ShoppingListItem的類,它封裝了購物列表中商品的名稱、數量和購買狀態:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

因為ShoppingListItem類的所有屬性都有默認值,而且它是一個沒有超類的基類,所以ShoppingListItem自動獲得一個默認初始化器實現,該實現創建一個新實例,所有屬性都設置為默認值。(name屬性是一個可選的字符串屬性,因此它自動接收一個默認值nil,即使這個值不是在代碼中編寫的。)上面的示例使用ShoppingListItem類的默認初始化器來創建該類的一個新實例,并使用初始化器語法編寫為ShoppingListItem(),并將這個新實例分配給一個名為item的變量。

Memberwise Initializers for Structure Types 結構體類型的所有成員構造器

與默認構造器不同,無論結構體中存儲的屬性有沒有默認值,都會有一個接收所有成員初始化器。

所有成員構造器是初始化新結構實例的成員屬性的一種簡寫方法。新實例的屬性的初始值可以通過名稱傳遞給成員初始化器。

下面的示例定義了一個名為Size的結構,它具有兩個名為width和height的屬性。通過指定默認值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)

Initializer Delegation for Value Types 值類型的初始化器委托

初始化器可以調用其他初始化器來執行實例初始化的一部分。這個過程稱為初始化器委托,它避免了跨多個初始化器復制代碼。

對于值類型和類類型,初始化器委托如何工作以及允許使用何種形式的委托的規則是不同的。值類型(結構和枚舉)不支持繼承,因此它們的初始化器委托過程相對簡單,因為它們只能委托給它們自己提供的另一個初始化器。但是,類可以從其他類繼承,如繼承中所述。這意味著類有額外的責任來確保它們繼承的所有存儲屬性在初始化期間都分配了合適的值。下面的類繼承和初始化描述了這些職責。

對于值類型,你可以在自定義構造器中調用self.init

注意,如果您為值類型定義了自定義初始化器,那么您將不再能夠訪問該類型的默認初始化器(如果它是一個結構,則為memberwise初始化器)。此約束可防止使用自動初始化器之一的人意外地繞過復雜初始化器中提供的其他必要設置。

注意:如果您希望您的自定義值類型可以使用缺省初始化器和memberwise初始化器進行初始化,也可以使用您自己的自定義初始化器進行初始化,請將自定義初始化器編寫在擴展中,而不是作為值類型初始實現的一部分。有關更多信息,請參見 Extensions

下面的示例定義了一個自定義矩形結構來表示幾何矩形。這個例子需要兩個支持結構Size和Point,它們都為所有屬性提供0.0的默認值:

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

可以通過以下三種方法之一初始化下面的Rect結構:使用其默認的零初始化原點和大小屬性值,提供特定的原點和大小,或者提供特定的中心點和大小。這些初始化選項由三個自定義初始化器表示,它們是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)
    }
}
let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

Class Inheritance and Initialization 類繼承和初始化

在初始化過程中,必須為類的所有存儲屬性(包括該類從超類繼承的任何屬性)分配一個初始值。

Swift為類類型定義了兩種初始化器,以幫助確保所有存儲的屬性都接收到初始值。這些被稱為指定(designated)初始化器和便利(convenience )初始化器。

Designated Initializers and Convenience Initializers

指定初始化器和方便的初始化器

指定初始化器是類的主要初始化器。指定的初始化器將完全初始化該類引入的所有屬性,并調用適當的超類初始化器,以便在超類鏈上繼續初始化過程。

類往往只有很少的指定初始化器,而且一個類通常只有一個初始化器。指定的初始化器是進行初始化的“漏斗”點,初始化過程通過這些“漏斗”點在超類鏈上繼續進行。

每個類必須至少有一個指定的初始化器。在某些情況下,可以通過從超類繼承一個或多個指定的初始化器來滿足此需求,如下面的自動初始化器繼承中所述。

便利初始化器是次要的,支持類的初始化器。您可以定義一個便利初始化器,以便從與便利初始化器相同的類調用指定的初始化器,并將指定初始化器的一些參數設置為默認值。您還可以定義一個方便的初始化器,為特定的用例或輸入值類型創建該類的實例。

如果類不需要初始化器,則不必提供方便的初始化器。當一個通用初始化模式的快捷方式可以節省時間或者使類的初始化更清晰時,創建方便的初始化器。

Syntax for Designated and Convenience Initializers 指定的和方便的初始化器的語法

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

init(parameters) {
statements
}

便利初始化器是用相同的風格編寫的,但是在init關鍵字前加上 convenience 修飾符,并用空格隔開:
convenience init(parameters) {
statements
}

Initializer Delegation for Class Types

為了簡化指定初始化器和便利初始化器之間的關系,Swift對初始化器之間的委托調用應用了以下三條規則:

規則1

指定初始化器必須從其直接超類調用指定初始化器。

規則2

一個方便的初始化器必須從同一個類調用另一個初始化器。

規則3

方便的初始化器最終必須調用指定的初始化器。

記住這一點的一個簡單方法是:

  • 指定的初始化程序必須始終向上委托。
  • 便利初始化器必須始終跨委托。

這些規則如下圖所示:

initializerDelegation01_2x.png

在這里,超類有一個指定的初始化器和兩個方便的初始化器。一個方便的初始化器調用另一個方便的初始化器,后者反過來調用單個指定的初始化器。這滿足上面的規則2和規則3。超類本身沒有進一步的超類,因此規則1不適用。

圖中的子類有兩個指定的初始化器和一個方便的初始化器。便利初始化器必須調用兩個指定初始化器中的一個,因為它只能從同一個類調用另一個初始化器。這滿足上面的規則2和規則3。兩個指定初始化器都必須從超類調用一個指定初始化器,以滿足上面的規則1。

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

下圖顯示了四個類的更復雜的類層次結構。它說明了這個層次結構中指定的初始化器如何作為類初始化的“漏斗”點,簡化了鏈中類之間的相互關系:

initializerDelegation02_2x.png

Two-Phase Initialization 兩階段初始化

Swift中的類初始化是一個兩階段的過程。在第一個階段中,每個存儲的屬性由引入它的類分配一個初始值。一旦確定了每個存儲屬性的初始狀態,第二個階段就開始了,每個類都有機會在新實例準備使用之前進一步定制其存儲屬性。

使用兩階段初始化過程使初始化安全,同時仍然為類層次結構中的每個類提供完全的靈活性。兩階段初始化可以防止屬性值在初始化之前被訪問,也可以防止屬性值被另一個初始化器意外地設置為不同的值。

Swift的兩階段初始化過程類似于Objective-C中的初始化。主要的區別在于,在第1階段,Objective-C為每個屬性分配0或null值(比如0或nil)。Swift的初始化流更靈活,因為它允許您設置自定義初始值,并且可以處理0或nil不是有效默認值的類型。

Swift的編譯器執行了四個有用的安全檢查,以確保兩階段初始化沒有錯誤地完成:

Safety check 1

指定的初始化器必須確保在將其委托給超類初始化器之前初始化其類引入的所有屬性。

如上所述,只有在知道對象所有存儲屬性的初始狀態之后,才會認為對象的內存已經完全初始化。為了滿足這個規則,指定的初始化器必須確保在傳遞到鏈之前初始化了它自己的所有屬性。

Safety check 2

指定的初始化器必須委托給超類初始化器,然后才給繼承的屬性賦值。如果沒有,指定初始化器分配的新值將被超類覆蓋,作為其初始化的一部分。

Safety check 3

在為任何屬性(包括由相同類定義的屬性)賦值之前,方便的初始化器必須委托給另一個初始化器。如果沒有,則便捷性初始化器分配的新值將被該類的指定初始化器覆蓋。

Safety check 4

在初始化的第一階段完成之前,初始化器不能調用任何實例方法、讀取任何實例屬性的值或將self引用為值。

直到第一個階段結束,類實例才完全有效。只有在知道類實例在第一階段結束時有效時,才能訪問屬性和調用方法。

基于以上四項安全檢查,以下是兩階段初始化的執行過程:

第一階段

  • 在類上調用指定的或方便的初始化器。
  • 為該類的新實例分配內存。內存還沒有初始化。
  • 該類的指定初始化器確認該類引入的所有存儲屬性都有一個值。現在初始化這些存儲屬性的內存。
  • 指定的初始化器傳遞給超類初始化器,以對其自身存儲的屬性執行相同的任務。
  • 這將繼續沿著類繼承鏈向上,直到到達鏈的頂部。
  • 一旦到達鏈的頂部,并且鏈中的最后一個類確保其所有存儲的屬性都有一個值,則認為實例的內存已經完全初始化,并且階段1已經完成。

第二階段

  • 從鏈的頂部向下工作,鏈中的每個指定初始化器都有進一步定制實例的選項。初始化器現在能夠訪問self并修改它的屬性、調用它的實例方法等等。
  • 最后,鏈中的任何便利初始化器都有自定義實例和使用self的選項。

下面是第1階段如何尋找一個假設子類和超類的初始化調用:

twoPhaseInitialization01_2x.png

超類的指定初始化器現在有機會進一步定制實例(盡管不必這樣做)。

一旦超類的指定初始化器完成,子類的指定初始化器就可以執行額外的自定義(盡管它也不必這樣做)。

最后,一旦子類的指定初始化器完成,最初調用的便利初始化器就可以執行額外的定制。

Initializer Inheritance and Overriding 初始化器繼承和覆蓋

與Objective-C中的子類不同,Swift子類默認情況下不繼承它們的超類初始化器。Swift的方法可以防止超類的簡單初始化器被更專門化的子類繼承,并用于創建未完全或正確初始化的子類的新實例。

超類初始化器在某些情況下是繼承的,但只有在這樣做是安全且合適的時候才會繼承。

如果希望自定義子類顯示與其超類相同的一個或多個初始化項,可以在子類中提供這些初始化項的自定義實現。

當您編寫與超類指定的初始化器匹配的子類初始化器時,您實際上是在覆蓋指定的初始化器。因此,必須在子類的初始化器定義之前編寫覆蓋修飾符。

與覆蓋的屬性、方法或下標一樣,覆蓋修飾符的存在提示Swift檢查超類是否有匹配的指定初始化器要被覆蓋,并驗證覆蓋初始化器的參數是否已按預期指定。

當重寫超類指定的初始化器時,總是要編寫覆蓋修飾符,即使子類的初始化器實現是一個方便的初始化器。

相反,如果您編寫的子類初始化器與超類便利初始化器匹配,則子類永遠不能直接調用該超類便利初始化器,這與上面在類類型初始化器委托中描述的規則相同。因此,子類(嚴格地說)不提供超類初始化器的覆蓋。因此,在提供超類便利初始化器的匹配實現時,不需要編寫覆蓋修飾符。

下面的示例定義了一個名為Vehicle的基類。這個基類聲明了一個名為numberOfWheels的存儲屬性,默認Int值為0。numberOfWheels屬性被一個名為description的計算屬性用來創建車輛特性的字符串描述:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Vehicle類為其惟一存儲的屬性提供默認值,本身不提供任何自定義初始化器。因此,它自動接收默認初始化器,如默認初始化器中所述。默認初始化器(如果可用)始終是類的指定初始化器,可以用來創建一個新的Vehicle實例,其車輪數為0:

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

下一個例子定義了一個名為Bicycle的車輛子類:

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

自行車子類定義了一個定制的指定初始化器init()。這個指定初始化器與Bicycle超類中的指定初始化器匹配,因此此初始化器的自行車版本使用覆蓋修飾符標記。

自行車的init()初始化器首先調用super.init(),它調用自行車類的超類Vehicle的默認初始化器。這確保了繼承的屬性numberOfWheels在bike有機會修改屬性之前由Vehicle初始化。在調用super.init()之后,numberOfWheels的原始值被替換為一個新值2。

如果你創建了一個自行車實例,你可以調用它的繼承描述計算屬性來查看它的numberOfWheels屬性是如何更新的:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

如果子類初始化器在初始化過程的第2階段沒有執行自定義,而超類有一個指定為零參數的初始化器,則可以在為所有子類的存儲屬性賦值之后省略對super.init()的調用。

這個例子定義了另一個Vehicle的子類,稱為Hoverboard。在初始化器中,懸浮板類只設置它的color屬性。這個初始化器不是顯式地調用super.init(),而是依賴于對其超類的初始化器的隱式調用來完成這個過程。

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

懸浮滑板Hoverboard的實例使用車輛初始化器提供的默認車輪數。

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

子類可以在初始化期間修改繼承的變量屬性,但不能修改繼承的常量屬性。

Automatic Initializer Inheritance 自動初始化繼承

如上所述,子類默認情況下不繼承它們的超類初始化器。但是,如果滿足某些條件,超類初始化器將自動繼承。在實踐中,這意味著您不需要在許多常見的場景中編寫初始化器覆蓋,并且可以在安全的情況下以最小的努力繼承超類初始化器。

假設您為您在子類中引入的任何新屬性提供默認值,則應用以下兩條規則:

規則1

如果子類沒有定義任何指定的初始化器,它將自動繼承其所有超類指定的初始化器。

規則2

如果子類提供了所有超類指定的初始化器的實現——要么按照規則1繼承它們,要么作為定義的一部分提供自定義實現——那么它將自動繼承所有超類便利初始化器。

即使子類添加了更方便的初始化器,這些規則仍然適用。

注意:作為滿足規則2的一部分,子類可以實現超類指定的初始化器作為子類便利初始化器。

Designated and Convenience Initializers in Action 指定的和方便的初始化作用

下面的示例顯示了指定的初始化器、方便的初始化器和正在運行的自動初始化器繼承。這個例子定義了一個由三個類組成的層次結構,分別是Food、recipecomponent和ShoppingListItem,并演示了它們的初始化器是如何交互的。

層次結構中的基類稱為Food,這是一個封裝食物名稱的簡單類。Food類引入了一個名為name的字符串屬性,并為創建Food實例提供了兩個初始化器

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

下圖顯示了Food類的初始化器鏈:


initializersExample01_2x.png

類沒有默認的成員初始化器,因此Food類提供了一個指定的初始化器,它只接受一個名為name的參數。該初始化器可用于創建一個具有特定名稱的新食品實例:

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

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

Food類還提供了一個方便的初始化器init(),沒有參數。init()初始化器為一個新食物提供了一個默認占位符名稱,方法是將其委托給食物類的init(name: String),其名稱值為[未命名]:

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

層次結構中的第二類是食物的子類RecipeIngredient。RecipeIngredient類為烹飪菜譜中的一個成分建模。它引入了一個名為quantity的Int屬性(除了從食物中繼承的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 類的初始化器鏈:


initializersExample02_2x.png

RecipeIngredient類有一個指定的初始化器init(名稱:String,數量:Int),它可以用來填充新recipe配料實例的所有屬性。這個初始化器首先將傳遞的quantity參數分配給quantity屬性,這是RecipeIngredient引入的惟一一個新屬性。這樣做之后,初始化器將委托給Food類的初始化器(名稱:String)。此過程滿足上述兩階段初始化的安全檢查1。

RecipeIngredient還定義了一個方便的初始化器init(name: String),它用于僅根據名稱創建RecipeIngredient實例。這個方便的初始化器假設對于任何沒有顯式數量創建的RecipeIngredient實例,數量為1。這個便利初始化器的定義使RecipeIngredient實例的創建更加快捷和方便,并且在創建多個單數量的RecipeIngredient實例時避免了代碼重復。這個方便的初始化器簡單地委托給類的指定初始化器,傳遞一個量值1。

RecipeIngredient提供的init(name: String)便利初始化器使用與食物中的init(name: String)指定初始化器相同的參數。因為這個方便的初始化器從它的超類中覆蓋指定的初始化器,所以必須用覆蓋修飾符標記它(如初始化器繼承和覆蓋中描述的那樣)。

盡管RecipeIngredient 提供init(name: String)初始化器作為一種方便的初始化器,但是RecipeIngredient 仍然提供了它的所有超類的指定初始化器的實現。因此,RecipeIngredient 也自動繼承了它的超類的所有便利初始化器。

在這個例子中,RecipeIngredient的超類是Food,它有一個簡單的初始化器init()。因此,RecipeIngredient繼承了這個初始化器。init()函數的繼承版本與食品版本的方法完全相同,只是它委托給init的RecipeIngredient版本(名稱:String),而不是食品版本。

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

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

層次結構中的第三個也是最后一個類是RecipeIngredient的子類ShoppingListItem。ShoppingListItem類對出現在購物列表中的菜譜成分建模。

購物清單上的每一項都以“未購買”開始。為了表示這一事實,ShoppingListItem引入了一個名為purchase的布爾屬性,默認值為false。ShoppingListItem還添加了一個computed description屬性,它提供了一個ShoppingListItem實例的文本描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ?" : " ?"
        return output
    }
}

注意:ShoppingListItem沒有定義初始化器來為已購買的商品提供初始值,因為購物列表中的商品(如這里所建模的)一開始總是未購買的。

因為ShoppingListItem為它所引入的所有屬性提供了一個默認值,并且本身沒有定義任何初始化器,所以ShoppingListItem自動地從它的超類繼承所有指定的和方便的初始化器。

下圖顯示了所有三個類的整體初始化鏈:


initializersExample03_2x.png

您可以使用所有三個繼承的初始化器來創建一個新的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 ?

這里,一個名為早餐列表的新數組是由一個包含三個新的ShoppingListItem實例的數組文本創建的。數組的類型被推斷為[ShoppingListItem]。創建數組后,數組開始處的ShoppingListItem名稱將從“[未命名]”更改為“Orange juice”,并標記為已購買。打印數組中每一項的描述將顯示它們的默認狀態已按預期設置。

Failable Initializers 可失敗的構造器

有時,定義初始化可能失敗的類、結構或枚舉是有用的。此故障可能由無效的初始化參數值、缺少所需的外部資源或其他阻止初始化成功的條件觸發。

要處理可能失敗的初始化條件,請將一個或多個可失敗的初始化器定義為類、結構或枚舉定義的一部分。通過在init關鍵字(init?)后面加上問號,可以編寫一個失敗的初始化器。

注意:不能使用相同的參數類型和名稱定義可失敗初始化器和不可失敗初始化器。

可失敗初始化器創建其初始化類型的可選值。在可失敗初始化器中寫入返回nil,以指示可以觸發初始化失敗的點。

嚴格地說,初始化器不返回值。相反,它們的作用是確保在初始化結束時完全正確地初始化了self。雖然您編寫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)")
}
// Prints "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int

if valueChanged == nil {
    print("\(pi) conversion to Int does not maintain value")
}
// Prints "3.14159 conversion to Int does not maintain value"

下面的示例定義了一個名為Animal的結構,它具有一個名為species的常量字符串屬性。動物結構還定義了一個可失敗的初始化器,該初始化器只有一個名為species的參數。此初始化器檢查傳遞給初始化器的物種值是否為空字符串。如果找到空字符串,將觸發初始化失敗。否則,設置物種屬性的值,初始化成功:

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

您可以使用這個失敗初始化器來嘗試初始化一個新的動物實例,并檢查初始化是否成功:

let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe"

如果將空字符串值傳遞給失敗初始化器的物種參數,初始化器將觸發初始化失敗:

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// Prints "The anonymous creature could not be initialized"

在上面的例子中,空字符串("")是一個有效的、非可選的字符串。但是,動物不能將空字符串作為其物種屬性的值。要對這個限制進行建模,如果找到空字符串,可失敗初始化器將觸發初始化失敗。

Failable Initializers for Enumerations 枚舉可失敗的構造器

可以使用可失敗構造器根據一個或多個參數選擇適當的枚舉用例。如果提供的參數不匹配適當的枚舉用例,初始化器可能會失敗。

下面的示例定義了一個名為TemperatureUnit的枚舉,它具有三種可能的狀態(kelvin 開爾文, celsius攝氏, fahrenheit華氏)。可失敗初始化程序用于為表示溫度符號的字符值找到合適的枚舉用例:

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.")
}
//成功

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
//失敗

Failable Initializers for Enumerations with Raw Values

用于具有原始值的枚舉的可失敗初始化器

帶有原始值的枚舉將自動接收一個可失敗初始化器init?(rawValue:),該初始化器接受一個名為rawValue的參數,該參數具有適當的raw-value類型,如果找到匹配的枚舉,則選擇匹配的枚舉用例;如果不存在匹配值,則觸發初始化失敗。

您可以重寫上面的TemperatureUnit示例,使用類型為Character的原始值,并利用init?(rawValue:)初始化器:

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.")
}
// Prints "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.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

Propagation of Initialization Failure 初始化失敗的傳播

類、結構或枚舉的可失敗初始化器可以從相同的類、結構或枚舉委托到另一個可失敗初始化器。類似地,子類可失敗初始化器可以委托給超類可失敗初始化器。

在這兩種情況下,如果您將委托給導致初始化失敗的另一個初始化器,則整個初始化過程將立即失敗,并且不會執行進一步的初始化代碼。

可失敗初始化器也可以委托給不可失敗初始化器。如果需要將潛在的失敗狀態添加到現有的初始化過程中,否則不會失敗,請使用此方法。

下面的示例定義了一個名為CartItem的產品子類。CartItem類為在線購物車中的項目建模。CartItem引入了一個名為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的故障初始化器首先驗證它接收到的數量值為1或更多。如果數量無效,則整個初始化過程將立即失敗,并且不再執行進一步的初始化代碼。同樣,Product的failable初始化器檢查name值,如果name是空字符串,初始化器進程將立即失敗。

如果使用非空名稱和數量為1或更多的CartItem實例,初始化成功:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"

如果您試圖創建一個數量值為0的CartItem實例,CartItem初始化器會導致初始化失敗:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// Prints "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")
}
// Prints "Unable to initialize one unnamed product"

Overriding a Failable Initializer 覆蓋可失敗的構造器

您可以像覆蓋任何其他初始化器一樣,覆蓋子類中的超類故障初始化器。或者,您可以使用子類不可失敗初始化器覆蓋超類可失敗初始化器。這使您能夠定義初始化不能失敗的子類,即使允許父類的初始化失敗。

注意,如果使用不可失敗的子類初始化器覆蓋可失敗超類初始化器,則將委托給超類初始化器的唯一方法是強制打開可失敗超類初始化器的結果。

您可以使用不可失敗初始化器覆蓋可失敗初始化器,但反之則不行。

下面的示例定義了一個名為Document的類。這個類為一個文檔建模,該文檔可以用name屬性初始化,該屬性要么是非空字符串值,要么為空,但不能是空字符串:

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

下一個示例定義了一個名為AutomaticallyNamedDocument的文檔子類。AutomaticallyNamedDocument子類覆蓋Document引入的兩個指定初始化器。這些覆蓋確保AutomaticallyNamedDocument實例的初始名值為“[Untitled]”,如果實例初始化時沒有名稱,或者一個空字符串被傳遞給init(name:)初始化器:

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:)初始化器。因為AutomaticallyNamedDocument處理空字符串的方式與其超類不同,所以它的初始化器不需要失敗,因此它提供了初始化器的一個不可失敗版本。

可以在初始化器中使用強制展開來從超類調用可失敗初始化器,作為子類不可失敗初始化器實現的一部分。例如,下面的UntitledDocument子類總是被命名為“[Untitled]”,它在初始化過程中使用父類中的可失敗init(name:)初始化器。

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}

在本例中,如果調用父類的init(name:)初始化器時使用空字符串作為名稱,則強制展開操作將導致運行時錯誤。但是,因為它是用字符串常量調用的,所以您可以看到初始化器不會失敗,所以在這種情況下不會發生運行時錯誤。

The init! Failable Initializer

您通常定義一個可失敗的初始化器,它通過在init關鍵字(init?)后面放置一個問號來創建適當類型的可選實例。或者,您可以定義一個可失敗的初始化器,該初始化器創建一個隱式的未包裝的可選實例,該實例具有適當的類型。通過在init關鍵字(init!)后面加上感嘆號而不是問號來實現這一點。

你可以從init中委托?初始化!反之亦然,你能重寫init嗎?初始化!反之亦然。您還可以將init委托給init!,盡管這樣做將觸發一個斷言,如果初始化!初始化器導致初始化失敗。

Required Initializers 必須的構造器

在類初始化器定義之前編寫所需的修飾符,以指示類的每個子類都必須實現該初始化器:

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

您還必須在必需初始化器的每個子類實現之前編寫必需的修飾符,以指示初始化器需求適用于鏈中的其他子類。當重寫所需的指定初始化器時,不需要編寫覆蓋修飾符:

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

注意:如果可以使用繼承的初始化器滿足需求,則不必提供所需初始化器的顯式實現。

Setting a Default Property Value with a Closure or Function 使用閉包或者函數設置默認屬性值

如果存儲屬性的默認值需要一些定制或設置,可以使用閉包或全局函數為該屬性提供定制的默認值。無論何時初始化屬性所屬類型的新實例,都會調用閉包或函數,并將其返回值指定為屬性的默認值。

這些類型的閉包或函數通常創建與屬性類型相同的臨時值,將該值調整為表示所需的初始狀態,然后返回該臨時值作為屬性的默認值。

下面是如何使用閉包來提供默認屬性值的概要:

class SomeClass {
    let someProperty: SomeType = {
        ...
        return someValue
    }()
}
注意,閉包的結束大括號后面跟著一對空括號。這告訴Swift立即執行閉包。如果省略這些括號,則嘗試將閉包本身分配給屬性,而不是閉包的返回值。
注意:如果使用閉包初始化屬性,請記住,在執行閉包時還沒有初始化實例的其余部分。這意味著您不能從閉包中訪問任何其他屬性值,即使這些屬性具有默認值。您也不能使用隱式self屬性,或調用實例的任何方法。

下面的示例定義了一個稱為棋盤的結構,它為國際象棋游戲建模棋盤。國際象棋是在一塊8×8的棋盤上進行的,棋盤上有黑白相間的方塊。


chessBoard_2x.png

為了表示這個棋盤,棋盤結構有一個名為boardColors的屬性,它是由64個Bool值組成的數組。數組中的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]
    }
}

每當創建一個新的棋盤實例時,就會執行閉包,并計算和返回boardColors的默認值。上面示例中的閉包計算并設置名為temporaryBoard的臨時數組中董事會上的每個正方形的適當顏色,并在設置完成后返回這個臨時數組作為閉包的返回值。返回的數組值存儲在boardColors中,可以使用squareIsBlackAt(row:column:) 實用函數查詢:

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

<<返回目錄

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

推薦閱讀更多精彩內容