今天聊聊Swift中的構(gòu)造器,無論你是Swfit新手還是混跡論壇多年的老鳥,相信你看完本文之后一定會有所收獲。
我們將討論以下四部分內(nèi)容:
- designated initializer
- convenience initializer
- required 關(guān)鍵字
- protocol中的initializer
Designated Initializer
當我們創(chuàng)建一個class的時候,如果這個class擁有****未初始化的非Optional屬性****,則必須提供一個構(gòu)造器(稱為designated initializer)。這句話有兩個重點:
****1.未初始化;2.非Optional屬性。****
比較一下下面幾種寫法:
//A
class Person {
let name : String = ""
}
//B
class Person {
let name : String
}
//C
class Person {
var name : String?
}
//D
class Person {
let name : String?
}
A,C都可以正常編譯,B,D會報錯(error: class 'Person' has no initializers)。因為Swift不允許非Optional變量在沒有初始化的情況下使用或者訪問,這樣會造成潛在的風險,于是Swift編譯器對構(gòu)造器做了嚴格的限制:類必須提供一個designated initializer,并且初始化所有****未初始化的非Optional屬性****
A,B兩種寫法中,let換成var結(jié)果是一樣的。但是D比較特殊,在Swift中,常量在訪問和使用之前必須初始化(哪怕是Optional)
var p : String?
print(p)
//正常編譯
let p : String?
print(p)
//報錯:error: constant 'p' used before being initialized
由此可見我們上面的描述并不準確,應當是****未初始化的非Optional屬性或Optinal常量****。不過一般我們不會聲明一個Optional常量,另外這么說也是在太過拗口,所以就簡單稱之為****未初始化的非Optional屬性****吧(下文也會這樣表述)。
同樣,在子類初始化過程中,Swift也做了嚴格的限制
class Person {
let name : String
init(name : String){
self.name = name
}
}
class Engineer: Person {
var title : String
init(name: String, title: String) {
self.title = title
super.init(name: name)
}
}
****首先****,Engineer必須調(diào)用super.init,如果你這樣寫:
init(name: String, title: String) {
self.title = title
self.name = name
}
不好意思,self.name是常量,不可修改。即便我們將let name : String改為var name : String,仍會報錯,而且是兩個:
error: super.init isn't called on all paths before returning from initializererror: use of 'self' in property access 'name' before super.init initializes self
從這兩個錯誤信息可以看出,在子類的構(gòu)造器中,父類的成員變量必須通過父類的designated initializer進行初始化。也就是說,我們必須調(diào)用super.init(第一個error),而且在調(diào)用super.init之前不能使用和訪問任何父類的成員變量(第二個error)。
****其次****,super.init必須在子類所有****未初始化的非Optional屬性****初始化之后調(diào)用。
這跟我們以前所了解的不同,例如在JAVA的構(gòu)造器中我們會先調(diào)用super.init。Swift之所以這樣做,同樣是為了安全性。
假設(shè)我們要求在初始化一個Person之后,立刻輸出一些信息用來debug:
class Person {
var name : String
init(name : String){
self.name = name
printDescription()
}
func printDescription() -> () {
print("name=\(name)")
}
}
class Engineer: Person {
var title : String
init(name: String, title: String) {
self.title = title
super.init(name: name)
}
override func printDescription() -> () {
print("name=\(name), title=\(title)")
}
}
如果在Engineer的init中,我們首先調(diào)用super.init,那么我們就會在初始化title之前執(zhí)行子類的printDescription方法,這是Swift所不允許的。
Convenience Initializer
一個類可以提供多個designated initializer,但是designated initializer不可以調(diào)用designated initializer。
如果需要提供一個依賴designated initializer的構(gòu)造器,需要添加關(guān)鍵字convenience,稱為convenience initializer。
在上面的例子中,假設(shè)我們從文件中讀取人的信息,讀取到的是****“名字;職位;聯(lián)系方式”****格式的字符串。我們需要寫一個構(gòu)造器方法從這串文字中解析出相應的信息。
class Person {
let name : String
init(name : String){
self.name = name
}
convenience init(convertFromLiteral value : String){
var name = ""
let components = value.componentsSeparatedByString(";")
for component in components{
if(name.characters.count == 0){
name = component
break
}
}
self.init(name: name)
}
}
因為新的構(gòu)造器調(diào)用了self.init,因此需要添加關(guān)鍵字convenience,否則會報錯:error: designated initializer for 'Person' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?
一個類可以擁有多個convenience initializer,而且convenience initializer可以調(diào)用其他convenience initializer。
convenience initializer有以下約束:
- 必須調(diào)用當前類的designated initializer,調(diào)用父類的designated initializer是不行的。
- 只有當前類可以使用(子類也可以使用,不過需要特殊處理。見下文)。
Required 關(guān)鍵字
如果父類中的構(gòu)造器需要在子類中重寫,可以使用required關(guān)鍵字。子類必須重寫父類中標記為required的構(gòu)造器方法(不管是designated initializer還是convenience initializer)。
上面的例子中,init(convertFromLiteral value : String)并不適合子類,所以我們加上required關(guān)鍵字,編譯器會提示我們Engineer沒有實現(xiàn)required initializer。
class Engineer: Person {
var title : String
init(name: String, title: String) {
self.title = title
super.init(name: name)
}
required convenience init(convertFromLiteral value : String){
var name = ""
var title = ""
let components = value.componentsSeparatedByString(";")
for component in components{
if(name.characters.count == 0){
name = component
}else if(title.characters.count == 0){
title = component
break
}
}
self.init(name: name, title: title)
}
}
上文提到了convenience initializer只有當前類才能使用,Swift不允許子類通過父類構(gòu)造器進行初始化是顯而易見的。convenience initializer實際上更像一個方法,如果我們將某個convenience initializer所依賴的designated initializer標記為required,就相當于告訴了編譯器我們的子類一定重寫了這個designated initializer。也就是說,如果我們的子類通過這個convenience initializer進行初始化,最終會執(zhí)行子類中重寫的designated initializer。
說這么一大堆,用代碼看的更清楚:
class Person {
let name : String
required init(name : String){
self.name = name
}
convenience init(convertFromLiteral value : String){
var name = ""
let components = value.componentsSeparatedByString(";")
for component in components{
if(name.characters.count == 0){
name = component
break
}
}
self.init(name: name)
}
}
class Engineer: Person {
var title : String
required init(name: String) {
self.title = "unknow"
super.init(name: name)
}
init(name: String, title: String) {
self.title = title
super.init(name: name)
}
}
let peter = Person.init(convertFromLiteral: "peter;")
print(peter.name)
let tom = Engineer.init(convertFromLiteral: "tom;")
print(tom.name + ", " + tom.title)
將父類中convenience initializer所依賴的designated initializer標記為required,我們就可以通過該convenience initializer初始化子類了。
Protocol中的Initializer
Swift提供了很多包涵構(gòu)造器的protocol,如StringLiteralConvertible,DictionaryLiteralConvertible,ArrayLiteralConvertible等。
如果我們想通過如下方式初始化,我們就需要用到StringLiteralConvertible
let peter : Person = "Peter"
代碼如下:
class Person: StringLiteralConvertible{
let name: String
init(name value: String) {
self.name = value
}
required convenience init(stringLiteral value: String) {
self.init(name: value)
}
required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(name: value)
}
required convenience init(unicodeScalarLiteral value: String) {
self.init(name: value)
}
}
let peter : Person = "Peter"
是不是很cool?
需要注意的是,Swfit很好的控制了構(gòu)造器的作用域(默認情況下,convenience initializer是子類不可見的),但是
protocol中的方法是子類可見的。所以protocol中的構(gòu)造器方法都必須聲明為required。
如果實現(xiàn)protocol的是一個final類,那么則沒必要再是required,因為它不會有子類。
等一下,如果Person是第三方庫中的類呢?
你可能會說沒問題啊,我們可以通過extension實現(xiàn)StringLiteralConvertible
class Person: StringLiteralConvertible{
let name: String
init(name value: String) {
self.name = value
}
}
extension Person: StringLiteralConvertible{
required convenience init(stringLiteral value: String) {
self.init(name: value)
}
required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(name: value)
}
required convenience init(unicodeScalarLiteral value: String) {
self.init(name: value)
}
}
let peter : Person = "Peter"
真是這樣嗎?Sorry!!!
- 首先,required initializer必須在類中聲明。所以上面的
required關(guān)鍵字要不得 - 其次,
protocol中的構(gòu)造器方法必須聲明為required,除非Person是final的。
emmm....
What Else
struct和enum同樣可以擁有構(gòu)造器,不過上面提到的designated initializer,convenience initializer,required 關(guān)鍵字都是針對class的,不要搞混。
最后上段代碼,供大家把玩
enum Sex{
case Male
case Female
case Unknow
}
let malesName = ["peter", "tom"]
let femalesName = ["marry", "taylor"]
extension Sex: StringLiteralConvertible{
init(stringLiteral value: String) {
if(malesName.contains(value)){
self = .Male
}else if(femalesName.contains(value)){
self = .Female
}else{
self = .Unknow
}
print(self)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}
init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}
var sex : Sex = "tom"
sex = "marry"
sex = "gollum"