類的繼承和構造過程
類裏面的所有存儲型屬性--包括所有繼承自父類的屬性--都必須在構造過程中設置初始值。
Swift 提供了兩種類型的類構造器來確保所有類實例中存儲型屬性都能獲得初始值,它們分別是指定構造器和便利構造器。
指定構造器和便利構造器
指定構造器是類中最主要的構造器。一個指定構造器將初始化類中提供的所有屬性,並根據父類鏈往上調用父類的構造器來實現父類的初始化。
每一個類都必須擁有至少一個指定構造器。在某些情況下,許多類通過繼承了父類中的指定構造器而滿足了這個條件。具體內容請參考後續章節自動構造器的繼承。
便利構造器是類中比較次要的、輔助型的構造器。你可以定義便利構造器來調用同一個類中的指定構造器,併爲其參數提供默認值。你也可以定義便利構造器來創建一個特殊用途或特定輸入的實例。
你應當只在必要的時候爲類提供便利構造器,比方說某種情況下通過使用便利構造器來快捷調用某個指定構造器,能夠節省更多開發時間並讓類的構造過程更清、晰明。
構造器鏈
爲了簡化指定構造器和便利構造器之間的調用關係,Swift 採用以下三條規則來限制構造器之間的代理調用:
規則 1
指定構造器必須調用其直接父類的的指定構造器。
規則 2
便利構造器必須調用同一類中定義的其它構造器。
規則 3
便利構造器必須最終以調用一個指定構造器結束。
一個更方便記憶的方法是:
指定構造器必須總是向上代理
便利構造器必須總是橫向代理
這些規則可以通過下面圖例來說明:
如圖所示,父類中包含一個指定構造器和兩個便利構造器。其中一個便利構造器調用了另外一個便利構造器,而後者又調用了唯一的指定構造器。這滿足了上面提到的規則2和3。這個父類沒有自己的父類,所以規則1沒有用到。
子類中包含兩個指定構造器和一個便利構造器。便利構造器必須調用兩個指定構造器中的任意一個,因爲它只能調用同一個類裏的其他構造器。這滿足了上面提到的規則2和3。而兩個指定構造器必須調用父類中唯一的指定構造器,這滿足了規則1。
注意:
這些規則不會影響使用時,如何用類去創建實例。任何上圖中展示的構造器都可以用來完整創建對應類的實例。這些規則只在實現類的定義時有影響。
下面圖例中展示了一種更復雜的類層級結構。它演示了指定構造器是如果在類層級中充當“管道”的作用,在類的構造器鏈上簡化了類之間的內部關係。
兩段式構造過程
Swift 中類的構造過程包含兩個階段。第一個階段,每個存儲型屬性通過引入它們的類的構造器來設置初始值。當每一個存儲型屬性值被確定後,第二階段開始,它給每個類一次機會在新實例準備使用之前進一步定製它們的存儲型屬性。
兩段式構造過程的使用讓構造過程更安全,同時在整個類層級結構中給予了每個類完全的靈活性。兩段式構造過程可以防止屬性值在初始化之前被訪問;也可以防止屬性被另外一個構造器意外地賦予不同的值。
注意:
Swift的兩段式構造過程跟 Objective-C中的構造過程類似。最主要的區別在於階段 1,Objective-C 給每一個屬性賦值0或空值(比如說0或nil)。Swift 的構造流程則更加靈活,它允許你設置定製的初始值,並自如應對某些屬性不能以0或nil作爲合法默認值的情況。
Swift 編譯器將執行 4 種有效的安全檢查,以確保兩段式構造過程能順利完成:
安全檢查 1
指定構造器必須保證它所在類引入的所有屬性都必須先初始化完成,之後才能將其它構造任務向上代理給父類中的構造器。
如上所述,一個對象的內存只有在其所有存儲型屬性確定之後才能完全初始化。爲了滿足這一規則,指定構造器必須保證它所在類引入的屬性在它往上代理之前先完成初始化。
安全檢查 2
指定構造器必須先向上代理調用父類構造器,然後再爲繼承的屬性設置新值。如果沒這麼做,指定構造器賦予的新值將被父類中的構造器所覆蓋。
安全檢查 3
便利構造器必須先代理調用同一類中的其它構造器,然後再爲任意屬性賦新值。如果沒這麼做,便利構造器賦予的新值將被同一類中其它指定構造器所覆蓋。
安全檢查 4
構造器在第一階段構造完成之前,不能調用任何實例方法、不能讀取任何實例屬性的值,也不能引用self的值。
以下是兩段式構造過程中基於上述安全檢查的構造流程展示:
階段 1
某個指定構造器或便利構造器被調用;
完成新實例內存的分配,但此時內存還沒有被初始化;
指定構造器確保其所在類引入的所有存儲型屬性都已賦初值。存儲型屬性所屬的內存完成初始化;
指定構造器將調用父類的構造器,完成父類屬性的初始化;
這個調用父類構造器的過程沿着構造器鏈一直往上執行,直到到達構造器鏈的最頂部;
當到達了構造器鏈最頂部,且已確保所有實例包含的存儲型屬性都已經賦值,這個實例的內存被認爲已經完全初始化。此時階段1完成。
階段 2
從頂部構造器鏈一直往下,每個構造器鏈中類的指定構造器都有機會進一步定製實例。構造器此時可以訪問self、修改它的屬性並調用實例方法等等。
最終,任意構造器鏈中的便利構造器可以有機會定製實例和使用self。
在這個例子中,構造過程從對子類中一個便利構造器的調用開始。這個便利構造器此時沒法修改任何屬性,它把構造任務代理給同一類中的指定構造器。
如安全檢查1所示,指定構造器將確保所有子類的屬性都有值。然後它將調用父類的指定構造器,並沿着造器鏈一直往上完成父類的構建過程。
父類中的指定構造器確保所有父類的屬性都有值。由於沒有更多的父類需要構建,也就無需繼續向上做構建代理。
一旦父類中所有屬性都有了初始值,實例的內存被認爲是完全初始化,而階段1也已完成。
父類中的指定構造器現在有機會進一步來定製實例(儘管它沒有這種必要)。
一旦父類中的指定構造器完成調用,子類的構指定構造器可以執行更多的定製操作(同樣,它也沒有這種必要)。
最終,一旦子類的指定構造器完成調用,最開始被調用的便利構造器可以執行更多的定製操作。
構造器的繼承和重載
跟 Objective-C 中的子類不同,Swift 中的子類不會默認繼承父類的構造器。Swift 的這種機制可以防止一個父類的簡單構造器被一個更專業的子類繼承,並被錯誤的用來創建子類的實例。
假如你希望自定義的子類中能實現一個或多個跟父類相同的構造器--也許是爲了完成一些定製的構造過程--你可以在你定製的子類中提供和重載與父類相同的構造器。
如果你重載的構造器是一個指定構造器,你可以在子類裏重載它的實現,並在自定義版本的構造器中調用父類版本的構造器。
如果你重載的構造器是一個便利構造器,你的重載過程必須通過調用同一類中提供的其它指定構造器來實現。這一規則的詳細內容請參考構造器鏈。
注意:
與方法、屬性和下標不同,在重載構造器時你沒有必要使用關鍵字override。
自動構造器的繼承
如上所述,子類不會默認繼承父類的構造器。但是如果特定條件可以滿足,父類構造器是可以被自動繼承的。在實踐中,這意味着對於許多常見場景你不必重載父類的構造器,並且在儘可能安全的情況下以最小的代價來繼承父類的構造器。
假設要爲子類中引入的任意新屬性提供默認值,請遵守以下2個規則:
規則 1
如果子類沒有定義任何指定構造器,它將自動繼承所有父類的指定構造器。
規則 2
如果子類提供了所有父類指定構造器的實現--不管是通過規則1繼承過來的,還是通過自定義實現的--它將自動繼承所有父類的便利構造器。
即使你在子類中添加了更多的便利構造器,這兩條規則仍然適用。
注意:
子類可以通過部分滿足規則2的方式,使用子類便利構造器來實現父類的指定構造器。
指定構造器和便利構造器的語法
類的指定構造器的寫法跟值類型簡單構造器一樣:
init(parameters) {
statements
}
便利構造器也採用相同樣式的寫法,但需要在init關鍵字之前放置convenience關鍵字,並使用空格將它們倆分開:
convenience init(parameters) {
statements
}
指定構造器和便利構造器實戰
接下來的例子將在實戰中展示指定構造器、便利構造器和自動構造器的繼承。它定義了包含三個類Food、RecipeIngredient以及ShoppingListItem的類層次結構,並將演示它們的構造器是如何相互作用的。
類層次中的基類是Food,它是一個簡單的用來封裝食物名字的類。Food類引入了一個叫做name的String類型屬性,並且提供了兩個構造器來創建Food實例:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
類沒有提供一個默認的逐一成員構造器,所以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的子類RecipeIngredient。RecipeIngredient類構建了食譜中的一味調味劑。它引入了Int類型的數量屬性quantity(以及從Food繼承過來的name屬性),並且定義了兩個構造器來創建RecipeIngredient實例:
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
convenience init(name: String) {
self.init(name: name, quantity: 1)
}
RecipeIngredient類擁有一個指定構造器init(name:String, quantity: Int),它可以用來產生新RecipeIngredient實例的所有屬性值。這個構造器一開始先將傳入的quantity參數賦值給quantity屬性,這個屬性也是唯一在RecipeIngredient中新引入的屬性。隨後,構造器將任務向上代理給父類Food的init(name: String)。這個過程滿足兩段式構造過程中的安全檢查1。
RecipeIngredient也定義了一個便利構造器init(name:String),它只通過name來創建RecipeIngredient的實例。這個便利構造器假設任意RecipeIngredient實例的quantity爲1,所以不需要顯示指明數量即可創建出實例。這個便利構造器的定義可以讓創建實例更加方便和快捷,並且避免了使用重複的代碼來創建多個quantity爲 1 的RecipeIngredient實例。這個便利構造器只是簡單的將任務代理給了同一類裏提供的指定構造器。
注意,RecipeIngredient的便利構造器init(name: String)使用了跟Food中指定構造器init(name: String)相同的參數。儘管RecipeIngredient這個構造器是便利構造器,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。這個類構建了購物單中出現的某一種調味料。
購物單中的每一項總是從unpurchased未購買狀態開始的。爲了展現這一事實,ShoppingListItem引入了一個布爾類型的屬性purchased,它的默認值是false。ShoppingListItem還添加了一個計算型屬性description,它提供了關於ShoppingListItem實例的一些文字描述:
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name.lowercaseString)"
output += purchased ? " ✔" :" ✘"
return output
}
}
注意:
ShoppingListItem沒有定義構造器來爲purchased提供初始化值,這是因爲任何添加到購物單的項的初始狀態總是未購買。
由於它爲自己引入的所有屬性都提供了默認值,並且自己沒有定義任何構造器,ShoppingListItem將自動繼承所有父類中的指定構造器和便利構造器。
你可以使用全部三個繼承來的構造器來創建ShoppingListItem的新實例:
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orangejuice"
breakfastList[0].purchased = true
for item in breakfastList {
println(item.description)
}
// 1 x orange juice ✔
// 1 x bacon ✘
// 6 x eggs ✘
如上所述,例子中通過字面量方式創建了一個新數組breakfastList,它包含了三個新的ShoppingListItem實例,因此數組的類型也能自動推導爲ShoppingListItem[]。在數組創建完之後,數組中第一個ShoppingListItem實例的名字從[Unnamed]修改爲Orange juice,並標記爲已購買。接下來通過遍歷數組每個元素並打印它們的描述值,展示了所有項當前的默認狀態都已按照預期完成了賦值。
通過閉包和函數來設置屬性的默認值
如果某個存儲型屬性的默認值需要特別的定製或準備,你就可以使用閉包或全局函數來爲其屬性提供定製的默認值。每當某個屬性所屬的新類型實例創建時,對應的閉包或函數會被調用,而它們的返回值會當做默認值賦值給這個屬性。
這種類型的閉包或函數一般會創建一個跟屬性類型相同的臨時變量,然後修改它的值以滿足預期的初始狀態,最後將這個臨時變量的值作爲屬性的默認值進行返回。
下面列舉了閉包如何提供默認值的代碼概要:
class SomeClass {
let someProperty: SomeType = {
// 在這個閉包中給 someProperty 創建一個默認值
// someValue 必須和 SomeType 類型相同
return someValue
}()
}
注意閉包結尾的大括號後面接了一對空的小括號。這是用來告訴 Swift 需要立刻執行此閉包。如果你忽略了這對括號,相當於是將閉包本身作爲值賦值給了屬性,而不是將閉包的返回值賦值給屬性。
注意:
如果你使用閉包來初始化屬性的值,請記住在閉包執行時,實例的其它部分都還沒有初始化。這意味着你不能夠在閉包裏訪問其它的屬性,就算這個屬性有默認值也不允許。同樣,你也不能使用隱式的self屬性,或者調用其它的實例方法。
下面例子中定義了一個結構體Checkerboard,它構建了西洋跳棋遊戲的棋盤:
西洋跳棋遊戲在一副黑白格交替的 10x10 的棋盤中進行。爲了呈現這副遊戲棋盤,Checkerboard結構體定義了一個屬性boardColors,它是一個包含 100 個布爾值的數組。數組中的某元素布爾值爲true表示對應的是一個黑格,布爾值爲false表示對應的是一個白格。數組中第一個元素代表棋盤上左上角的格子,最後一個元素代表棋盤上右下角的格子。
boardColor數組是通過一個閉包來初始化和組裝顏色值的:
struct Checkerboard {
let boardColors: Bool[] = {
var temporaryBoard = Bool[]()
var isBlack = false
for i in 1...10 {
for j in 1...10 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
return boardColors[(row * 10) + column]
}
}
每當一個新的Checkerboard實例創建時,對應的賦值閉包會執行,一系列顏色值會被計算出來作爲默認值賦值給boardColors。上面例子中描述的閉包將計算出棋盤中每個格子合適的顏色,將這些顏色值保存到一個臨時數組temporaryBoard中,並在構建完成時將此數組作爲閉包返回值返回。這個返回的值將保存到boardColors中,並可以通squareIsBlackAtRow這個工具函數來查詢。
let board = Checkerboard()
println(board.squareIsBlackAtRow(0, column:1))
// 輸出 "true"
println(board.squareIsBlackAtRow(9, column:9))
// 輸出 "false"