構(gòu)造器(Initializer)
其實我個人并不喜歡把init和deinit叫做構(gòu)造器和析構(gòu)器(真正應(yīng)該對應(yīng)constructor和destructor), 因為構(gòu)造和析構(gòu)做的更多的應(yīng)該是分配和回收內(nèi)存, initializer應(yīng)該只是初始化器或初始化方法, 但是聽起來很奇怪而且也很麻煩, 加上Swift并不需要我們自己來手動分配和回收內(nèi)存, 所以還是以構(gòu)造器和析構(gòu)器來稱呼這2個概念吧.
前言
這一段與ObjC有很大的區(qū)別, 比如之前我們是先調(diào)用父類的init再寫自己的, 但是到了Swift里面, 我們卻先初始化自己, 再初始化父類, 是相反的, 具體為什么, WWDC的視頻里面有說, 總結(jié)起來有3點:
- Swift規(guī)定每個存儲屬性在被訪問之前都必須得到初始化(可選屬性如果是變量則會自動初始化為nil); // 1.30更新: 之前漏了 如果是變量, 因為常量是不會自動初始化為nil的, 即使是可選屬性, 也要自己手動初始化.
- Swift中初始化是兩段式的, 如果在第二階段訪問了一個方法, 如果這個方法被子類重載了, 而且方法里面訪問了一個子類的屬性, 就會出現(xiàn)和規(guī)則違背的地方;
- 那為什么ObjC可以呢? 原因是ObjC會為每一個屬性默認(rèn)設(shè)置為0或者nil, 但是Swift不會.
綜合上面3個原因, 導(dǎo)致出現(xiàn)了這個情況, 具體看一個例子吧
class Human {
var age:Int
init(){
age = 0
desc() // 如果初始化一個Man 對象, 這邊實際上會調(diào)用子類的desc方法,
//至于為什么還需要解釋嗎?
}
func desc(){
print("Human")
}
}
class Man : Human {
var prop: String
override func desc(){
print("Man \(prop)")
}
override init(){
prop = "123"
super.init()
}
}
這就是為什么要先初始化自身, 再初始化父類的原因, 其中還涉及到很多別的細(xì)節(jié), 例如繼承而來的屬性怎么算?還有具體里面的兩段式是什么之后再說, 我們先按官方文檔的節(jié)奏慢慢講.
為存儲屬性設(shè)置初始值(Setting Initial Values for Stored Properties)
如前面所述類和結(jié)構(gòu)體在創(chuàng)建實例的時候都必須給所有的存儲屬性設(shè)置初始值, 可以在構(gòu)造器中設(shè)置, 也可以在聲明屬性的時候就給定. 注意: 給定初始值不會觸發(fā)屬性
監(jiān)聽.
構(gòu)造器(initializers)
構(gòu)造器會在創(chuàng)建實例的時候自動調(diào)用, 一般來說每個類都需要構(gòu)造器, 無論是自己寫的還是編譯器為你生成的.
如果你為所有的屬性都設(shè)置了默認(rèn)值, 你可以選擇不手動寫一個構(gòu)造器. 可以用init關(guān)鍵字來聲明一個構(gòu)造器(之前說過不需要func修飾), 括號里面可以加各種參數(shù). 如:
init(){
}
init(name:String){
self.name = name
}
構(gòu)造參數(shù)
構(gòu)造器和函數(shù)很相似, 但也意味著構(gòu)造器和一般函數(shù)不同:
- 函數(shù)的第一個參數(shù)如果不手動寫標(biāo)簽是會忽略掉的, 而構(gòu)造器不會
- 函數(shù)可能有返回值, 但是構(gòu)造器沒有返回值(這就是另一個原因我不喜歡講initializer為構(gòu)造器的原因)
以官方的結(jié)構(gòu)體構(gòu)造器為例子:
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
// 在使用構(gòu)造器的時候, 即使只有一個參數(shù)也會有標(biāo)簽, 原因當(dāng)然是構(gòu)造器名字固定, 不能在后面加WithXXX了,
// 如果不想要可以和函數(shù)一樣, 用下劃線(_)來隱藏
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
可以看到, Swift里面構(gòu)造器都叫init, 而不是像ObjC一樣, 還有initWithXXX什么的, 所以構(gòu)造器(函數(shù)也是)是由名字和參數(shù)類型來統(tǒng)一標(biāo)識的.
可選類型與常量
可選類型會被默認(rèn)置為nil, 所以并不強制在初始化時賦值, 而常量則必須要初始化, 或者聲明的時候給默認(rèn)值.
默認(rèn)構(gòu)造器
在下面兩種情況下Swift會自動生成一個默認(rèn)構(gòu)造器
- 結(jié)構(gòu)體沒有手動聲明任何構(gòu)造器(結(jié)構(gòu)體很有意思, 如果全部的非可選屬性都有默認(rèn)值, 會生成兩個默認(rèn)構(gòu)造器, 其根本規(guī)則還是和之前說的一樣, 使用實例之前, 其存儲屬性必須全部得到初始化)
- 類里面每一個非可選屬性都有了默認(rèn)值, 且沒有聲明任何構(gòu)造器
值類型的構(gòu)造代理
所謂構(gòu)造代理就是構(gòu)造器可以調(diào)用別的構(gòu)造器來輔助完成構(gòu)造過程, 主要還是為了代碼復(fù)用.
構(gòu)造代理對值類型和引用類型來說不太一樣, 值類型因為不支持繼承, 所以只會用自己寫的構(gòu)造器來代理, 從而相對更簡單. 類則會有從父類繼承構(gòu)造器的情況要考慮, 不過還是那句話, 所有存儲屬性在構(gòu)造器中都完成初始化就可以.
值得一提的是, 如之前所說, 如果你自己實現(xiàn)了一個自定義的構(gòu)造器, 那么默認(rèn)的構(gòu)造器將不會被合成, 如果你還是想要默認(rèn)的構(gòu)造器, 就用extension來寫自定義構(gòu)造器即可.
直接上例子吧:
struct Size {
var width = 0.0, height = 0.0 // 全部有默認(rèn)值, 會生成2個構(gòu)造器
}
struct Point {
var x = 0.0, y = 0.0
}
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) // 構(gòu)造器代理
}
}
類繼承與初始化
再次強調(diào), 類的所有存儲屬性包括從父類繼承而來的都必須在初始化的時候賦予初值.
Swift中定義了兩種類型的構(gòu)造器來確保所有的存儲屬性都獲得初值, 即指派構(gòu)造器和便利構(gòu)造器.
指派構(gòu)造器和便利構(gòu)造器
指派構(gòu)造器是主要的構(gòu)造手段, 每個類至少要有1個, 意味著可以有多個, 但是多數(shù)情況下會是1個, 而便利構(gòu)造器則沒有要求.
對指派構(gòu)造器的要求則是必須真正完成所有存儲屬性的初始化, 而便利構(gòu)造器則要求最終要調(diào)用一個指派構(gòu)造器, 這兩者的關(guān)系比較像之前提到的構(gòu)造代理. 在一些細(xì)節(jié)上會有區(qū)分, 先慢慢來看.
從語法上, 如果僅僅是init, 就會是指派構(gòu)造器, 用convenience來修飾, 就是便利構(gòu)造器. 如:
init(parameters) {
statements
}
convenience init(parameters) {
statements
}
類類型的構(gòu)造代理
為了描述清楚指派構(gòu)造器和便利構(gòu)造器之間的關(guān)系, Swift有以下3個法則:
法則1:
一個指派構(gòu)造器必須調(diào)用它的直接父類的指派構(gòu)造器(至于為什么, 可以下面這個例子看看):
class Root {
var a : Int
init(){
a = 0
}
convenience init(i:Int){
self.init()
a = i
print(self) // 打印兩次, 第一次是Root, 第二次是Node
}
}
class Node : Root {
var b: Int
override init(){
b = 1
super.init()
}
}
var root = Root(i: 2)
var node = Node(i: 2)
法則2:
便利構(gòu)造器必須調(diào)用類中其它的構(gòu)造器
法則3:
便利構(gòu)造器必須最終調(diào)用一個指派構(gòu)造器(這個是必須的, 畢竟至于指派構(gòu)造器才要求初始化所有存儲屬性)
Tip: 其實一開始很奇怪為什么一定要有個便利構(gòu)造器的概念, 其實是因為Swift對初始化的檢查非常的嚴(yán)格(稍候還會講繼承和二段式), 所以便利構(gòu)造器就是為了不要反復(fù)進行檢查, 一般普遍的做法是給傳一些默認(rèn)參數(shù), 例如UIView中, 假如init(frame:CGRect)是指派構(gòu)造器, 那么init()就可以定義為便利構(gòu)造器, 直接調(diào)用self.init(frame:CGRectZero)即可
兩段式初始化
Swift中類的初始化分為兩段式:
- 第一階段, 每個存儲屬性都賦予初值
- 對實例進行進一步操作(例如加個監(jiān)聽什么的)
之所以有兩段式其實還是為了確保開發(fā)者不會在屬性未得到初始化就訪問屬性的情況.
所以, 可以預(yù)見的是, 在一個稍微復(fù)雜的類中, 至少會天然出現(xiàn)2段截然不同的代碼, 一段是不停地賦值, 一段是做一些其它的操作.
同時, Swift的編譯器會為兩段式初始化做一系列的檢查, 以確保初始化完成(這段比較重要, 所以直接全部翻譯了):
安全檢查1:
把構(gòu)造過程交給父類前, 指派構(gòu)造器必須保證聲明的存儲屬性都已初始化了.
如上所述, 一旦所有的存儲屬性的初始狀態(tài)都已知之后, 這個對象的內(nèi)存才被當(dāng)做已初始化. 為了滿足這個規(guī)則, 一個指派構(gòu)造函數(shù)在移交給初始化鏈之前, 必須確定自己本類所有的屬性都已經(jīng)初始化
安全檢查2:
一個指派構(gòu)造器必須把繼承屬性賦值之后才能移交給父類初始化函數(shù). 如果不這么做, 指派構(gòu)造器的賦的新值就會被父類的初始化函數(shù)給覆蓋
安全檢查3
一個便利初始化函數(shù)在給任何屬性賦值之前(包括自己類的屬性), 必須調(diào)用其它的初始化函數(shù). 如果不這么做, 便利初始化函數(shù)賦的值將會被指派初始化函數(shù)覆蓋
安全檢查4
初始化函數(shù)不能調(diào)用其它實例方法, 不能從實例屬性中取值, 也不能用self, 直到第一階段初始化完成(其實這里Swift做的不是特別嚴(yán)謹(jǐn), 如果一個類沒有父類, 那么即使還沒有初始化完成也可以用self, 這里只能勉強解釋為第一二階段合二為一了.)
我們再來看看這兩段初始化到底做了些什么事情:
階段1:
- 類的一個指派或便利初始化函數(shù)被調(diào)用
- 類實例的內(nèi)存被分配出來, 但是這塊內(nèi)存并沒有被初始化
- 一個類的指派初始化函數(shù)保證類所有的存儲屬性都有值. 到這一階段, 存儲屬性的內(nèi)存就已經(jīng)初始化了
- 這個指派構(gòu)造器把初始化進程移交給父類初始化函數(shù)來對父類存儲屬性實現(xiàn)同樣的操作
- 這個過程一直沿著繼承鏈持續(xù)下去, 直到達(dá)到繼承鏈頂端
- 到了繼承鏈頂端, 并且最終父類保證所有的存儲屬性都有值之后, 實例的內(nèi)存就被當(dāng)做完全初始化了, 此時階段1完成
階段2:
- 從繼承鏈頂端倒回來(因為函數(shù)調(diào)用棧啊), 每一個指派初始化函數(shù)都可以進一步定制實例, 初始化函數(shù)至此可以訪問self, 并且可以修改自己的屬性, 調(diào)用實例方法, 等等
- 最終, 調(diào)用鏈上的任意便利初始化方法都可以操作實例了, 也可以訪問self.
構(gòu)造器繼承和重載
Swift不會像ObjC一樣, 自動從父類那里繼承父類的構(gòu)造器, 因為Swift不像ObjC會給屬性默認(rèn)值, 如果繼承下來的話, 可能會有屬性沒有被初始化. 當(dāng)然只是說不會自動而已, 我們還是可以做一些事情來實現(xiàn)繼承在下一節(jié)會細(xì)講.
此外, 如果子類的指派構(gòu)造器和父類相同, 也要用override來修飾. 但是, 如果子類的指派構(gòu)造器與父類的便利構(gòu)造器相同, 那么父類的便利構(gòu)造器永遠(yuǎn)都不會被子類調(diào)用到(具體原因參考構(gòu)造代理法則1), 所以這種情況是不需要寫override的.
注意: 初始化時, 子類只可以修改繼承的變量屬性, 而不能修改繼承的常量
自動構(gòu)造器繼承
如上一節(jié)稍微提過的, 雖然默認(rèn)不能繼承, 但是只要滿足下面的2種情況就可以:
規(guī)則1:
子類沒有定義任何的指派構(gòu)造器, 那么就會自動從父類那里繼承所有的指派構(gòu)造器(例如, 所有的子類屬性都有默認(rèn)值)
規(guī)則2:
子類提供了全部的父類指派構(gòu)造器而不是從情況1獲得的, 即使是提供了一部分實現(xiàn), 那么它將自動繼承所有的父類便利構(gòu)造器. (個人認(rèn)為, 調(diào)用實現(xiàn)的那一個指派構(gòu)造器的便利構(gòu)造器都可以, 其余的不行可以更智能, 而且也不會出問題)
注意: 子類可以實現(xiàn)父類的指派構(gòu)造器為自己的便利構(gòu)造器, 從而滿足規(guī)則2來獲得父類的構(gòu)造器. 如:
class Root {
var a : Int
var b: Int
init(){
self.a = 0
self.b = 0
}
// 2. 解開這段注釋下面的node聲明不滿足規(guī)則1
// init(str:String){
// self.a = Int(str)!
// self.b = Int(str)!
// }
convenience init(i:Int){
self.init()
a = i
b = i
}
}
class Node : Root {
var bb: Int
override init() {
bb = 0
super.init()
}
// 3. 解開這段注釋將滿足規(guī)則2
// convenience override init(str:String){
// self.init()
//
// }
}
var node = Node(i: 3) // 1. 子類實現(xiàn)了父類的全部指派構(gòu)造器, 滿足規(guī)則1
指派構(gòu)造器和便利構(gòu)造器實例
文檔給出了一個例子來進行講解, 直接先看代碼:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() { // 便利構(gòu)造器用: 給出默認(rèn)參數(shù)
self.init(name: "[Unnamed]")
}
}
let namedMeat = Food(name: "Bacon")
let mysteryMeat = Food()
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) { // 重載父類的指派構(gòu)造器, 滿足規(guī)則2
self.init(name: name, quantity: 1)
}
}
let oneMysteryItem = RecipeIngredient() // 繼承而來的構(gòu)造器
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ?" : " ?"
return output
}
}
// 因為ShoppingListItem滿足規(guī)則1, 所以全部繼承下來了
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
構(gòu)造失敗(Failable initializer)
有時候需要返回構(gòu)造失敗, 例如傳入了不恰當(dāng)?shù)膮?shù). 這種情況主要是引起調(diào)用方的注意, 在傳入關(guān)鍵參數(shù)的時候要當(dāng)心.
可失敗的構(gòu)造器聲明也很簡單, 用init?就可以的, 但是要注意, 可失敗和不可失敗的構(gòu)造器不能有一樣的參數(shù)類型列表, 否則就有二義性了.
構(gòu)造失敗, 自然就是返回nil了, 所以可失敗的構(gòu)造器返回值是Optional的, 在使用的時候要注意拆包.
需要注意的是, 值類型和引用類型在處理失敗構(gòu)造的時候有些許不一樣, 先看值類型的:
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil } // return nil 在任意位置都可以
self.species = species
}
}
enum TemperatureUnit {
case Kelvin, Celsius, Fahrenheit
init?(symbol: Character) {
switch symbol {
case "Z":
return nil // 寫在哪里都是可以的
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}
// 枚舉還有另外一種寫法, 因為沒有可以通過rawValue來創(chuàng)建
enum TemperatureUnit: Character {
case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
TemperatureUnit(rawValue: "X") // 這里返回nil
類的失敗構(gòu)造
上一節(jié)可以看到, 值類型的失敗構(gòu)造可以發(fā)生在初始化進程的任何點上. 但是對于類而言, 只有在所有的存儲屬性都賦予了初值之后才能出發(fā)失敗構(gòu)造. 例如:
class Product {
let name: String! // 如果不記得這里的感嘆號代表什么就去看第一章
init?(name: String) {
self.name = name
if name.isEmpty { return nil }
}
}
其實個人不是很明白這里為什么要限定這么死, 但是官方文檔也沒有講更多的信息了, 只能當(dāng)做一項規(guī)定來看了.
構(gòu)造失敗的傳遞
類,結(jié)構(gòu)體和枚舉的構(gòu)造失敗是會傳遞到其它的構(gòu)造器去的, 而且子類構(gòu)造失敗還會傳遞到父類的可失敗構(gòu)造器中去. 從而整個初始化進程都會直接失敗掉, 不會再執(zhí)行其它的代碼了.
注意: 一個可失敗的構(gòu)造器也是可以調(diào)用非可失敗構(gòu)造器的. 通過這種方式, 我們可以在現(xiàn)有的構(gòu)造進程中添加一個潛在失敗狀態(tài).
以一個例子來講解:
class CartItem: Product {
let quantity: Int!
init?(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
if quantity < 1 { return nil }
}
}
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
print("Unable to initialize zero shirts") // 這里被打印
}
// 同樣的
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product") // 父類構(gòu)造失敗
}
重載可失敗的構(gòu)造器
和上面提到的其它構(gòu)造器重載沒有別的區(qū)別, 只是我們可以用一個不可失敗的構(gòu)造器來重載可失敗的構(gòu)造器, 但是不能反過來. 需要注意的是, 如果你重載了一個父類的可失敗構(gòu)造器, 記得要拆包. 例子:
class Fail{
var a:Int
init?(i:Int){
a = i
if i < 0 {return nil}
}
}
class Error : Fail{
var b: Int
override init(i:Int){
b = i
super.init(i:abs(i))! // 要加感嘆號, 當(dāng)然要確保父類可以構(gòu)造成功
}
}
init!可失敗構(gòu)造器
對于Optional type有兩種形式, 一種是?, 一種則是!. 兩者的具體區(qū)別在第一張的基礎(chǔ)篇已經(jīng)說過. 這里再強調(diào)一次吧. 用?的optional type, 在使用的時候需要拆包, 而用!的optional type可以直接使用, 就像ObjC里面的指針一樣. 需要注意的是, 不管哪種形式, 都是需要判空的, 否則會出現(xiàn)crash.
所以在init?和init!的區(qū)別也在于此, 這兩者在初始化的時候是可以互相代理的. 同時, 也可以在init里面把初始化交給init!代理, 只是這么做的話, 如果失敗就會觸發(fā)斷言失敗.
必需構(gòu)造器(Required Initializers)
如果init用了required來修飾, 那么意味著子類必需要重載這個構(gòu)造器.
不過值得一提的是, 如果你滿足構(gòu)造器繼承的條件的話, 必需構(gòu)造器也不是一定要實現(xiàn)的, 例如:
class Fail{
var a:Int
required init(){
a = 0
}
}
class Error : Fail{
var b: Int = 1
}
var err = Error()
err.a // 打印出0
用閉包或者函數(shù)來設(shè)置默認(rèn)值
在設(shè)置存儲屬性默認(rèn)值的時候, 可以用函數(shù)或者閉包來實現(xiàn), 例如官方的兩個例子:
// 閉包
class SomeClass {
let someProperty: SomeType = {
// 注意: 在這里不能用self, 更不能用其它的屬性(即使它有默認(rèn)值, 因為self還沒準(zhǔn)備好)或者該類的實例方法
// 也就是說執(zhí)行這段代碼時, 初始化都還沒有進行
return someValue
}() // 最后要加上(), 不然就被當(dāng)做是個閉包, 而不是這個閉包的結(jié)果了
}
// 函數(shù), 這個例子是創(chuàng)建一個國際象棋的棋盤, 黑白間隔開的
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]
}
}
至此, 初始化就結(jié)束了, 這一章總結(jié)的不好, 基本都是按照原文翻過來的, 因為的確是很多規(guī)則, 沒法解釋, 就是這么規(guī)定的. 如果一定要總結(jié)的話, 就是之前反復(fù)提到的那句話, Swift要求任何實例使用之前都要先初始化完成. 所有的那些規(guī)則基本上都是圍繞著這句話進行的.
官網(wǎng)上有一些圖表, 有興趣可以去看看官方文檔