這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔坐桩,今天18年5月份再次想寫(xiě)文章,發(fā)現(xiàn)簡(jiǎn)書(shū)還為我保存起的封锉,為了不辜負(fù)它的好意绵跷,也因?yàn)橐鼍壡珊希蔷桶l(fā)布出來(lái)吧
改一下工時(shí):嫫帧6镀骸!C撇妗2晾!N詹唷r乔啤!F非妗B窈稀!萄传!
Extention
try catch
rxSwift
internalpublicprivate
varlet
asas?強(qiáng)轉(zhuǎn)
? !
didSet
#selector
約束問(wèn)題
var myLabel : UILable ?//聲明全局變量myLabel
計(jì)算屬性/存儲(chǔ)屬性
基礎(chǔ)部分
1.可選類(lèi)型
2.if語(yǔ)句以及強(qiáng)制解析
3.隱式解析可選類(lèi)型
隱式解析可選類(lèi)型
如上所述甚颂,可選類(lèi)型暗示了常量或者變量可以“沒(méi)有值”。可選可以通過(guò)if語(yǔ)句來(lái)判斷是否有值振诬,如果有值的話可以通過(guò)可選綁定來(lái)解析值蹭睡。
有時(shí)候在程序架構(gòu)中,第一次被賦值之后赶么,可以確定一個(gè)可選類(lèi)型_總會(huì)_有值肩豁。在這種情況下,每次都要判斷和解析可選值是非常低效的辫呻,因?yàn)榭梢源_定它總會(huì)有值清钥。
這種類(lèi)型的可選狀態(tài)被定義為隱式解析可選類(lèi)型(implicitly unwrapped optionals)。把想要用作可選的類(lèi)型的后面的問(wèn)號(hào)(String?)改成感嘆號(hào)(String!)來(lái)聲明一個(gè)隱式解析可選類(lèi)型放闺。
當(dāng)可選類(lèi)型被第一次賦值之后就可以確定之后一直有值的時(shí)候祟昭,隱式解析可選類(lèi)型非常有用。隱式解析可選類(lèi)型主要被用在Swift中類(lèi)的構(gòu)造過(guò)程中雄人,請(qǐng)參考無(wú)主引用以及隱式解析可選屬性从橘。
一個(gè)隱式解析可選類(lèi)型其實(shí)就是一個(gè)普通的可選類(lèi)型,但是可以被當(dāng)做非可選類(lèi)型來(lái)使用础钠,并不需要每次都使用解析來(lái)獲取可選值恰力。下面的例子展示了可選類(lèi)型String和隱式解析可選類(lèi)型String之間的區(qū)別:
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! //需要驚嘆號(hào)來(lái)獲取值
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString//不需要感嘆號(hào)
你可以把隱式解析可選類(lèi)型當(dāng)做一個(gè)可以自動(dòng)解析的可選類(lèi)型。你要做的只是聲明的時(shí)候把感嘆號(hào)放到類(lèi)型的結(jié)尾旗吁,而不是每次取值的可選名字的結(jié)尾渣淳。
注意:
如果你在隱式解析可選類(lèi)型沒(méi)有值的時(shí)候嘗試取值柴墩,會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤。和你在沒(méi)有值的普通可選類(lèi)型后面加一個(gè)驚嘆號(hào)一樣。
你仍然可以把隱式解析可選類(lèi)型當(dāng)做普通可選類(lèi)型來(lái)判斷它是否包含值:
if assumedString != nil {
print(assumedString)
}
//輸出"An implicitly unwrapped optional string."
你也可以在可選綁定中使用隱式解析可選類(lèi)型來(lái)檢查并解析它的值:
if let definiteString = assumedString {
print(definiteString)
}
//輸出"An implicitly unwrapped optional string."
注意:
如果一個(gè)變量之后可能變成nil的話請(qǐng)不要使用隱式解析可選類(lèi)型砸西。如果你需要在變量的生命周期中判斷是否是nil的話蒿往,請(qǐng)使用普通可選類(lèi)型咽块。
4.錯(cuò)誤處理 添加throws關(guān)鍵詞來(lái)拋出錯(cuò)誤消息
5.使用斷言進(jìn)行調(diào)試let age = -3
assert(age >= 0, "A person's age cannot be less than zero")
基本運(yùn)算符
6.空合運(yùn)算符(Nil Coalescing Operator)a ?? b—>>a != nil ? a! : b
空合運(yùn)算符(a ?? b)將對(duì)可選類(lèi)型a進(jìn)行空判斷潮剪,如果a包含一個(gè)值就進(jìn)行解封,否則就返回一個(gè)默認(rèn)值b袁稽。表達(dá)式a必須是Optional類(lèi)型勿璃。默認(rèn)值b的類(lèi)型必須要和a存儲(chǔ)值的類(lèi)型保持一致。
空合運(yùn)算符是對(duì)以下代碼的簡(jiǎn)短表達(dá)方法:
a != nil ? a! : b
上述代碼使用了三目運(yùn)算符推汽。當(dāng)可選類(lèi)型a的值不為空時(shí)补疑,進(jìn)行強(qiáng)制解封(a!),訪問(wèn)a中的值歹撒;反之返回默認(rèn)值b莲组。無(wú)疑空合運(yùn)算符(??)提供了一種更為優(yōu)雅的方式去封裝條件判斷和解封兩種行為,顯得簡(jiǎn)潔以及更具可讀性暖夭。
注意:如果a為非空值(non-nil)锹杈,那么值b將不會(huì)被計(jì)算撵孤。這也就是所謂的短路求值。
下文例子采用空合運(yùn)算符嬉橙,實(shí)現(xiàn)了在默認(rèn)顏色名和可選自定義顏色名之間抉擇:
let defaultColorName = "red"
var userDefinedColorName: String?//默認(rèn)值為nil
var colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName的值為空早直,所以colorNameToUse的值為"red"
userDefinedColorName變量被定義為一個(gè)可選的String類(lèi)型,默認(rèn)值為nil市框。由于userDefinedColorName是一個(gè)可選類(lèi)型,我們可以使用空合運(yùn)算符去判斷其值糕韧。在上一個(gè)例子中枫振,通過(guò)空合運(yùn)算符為一個(gè)名為colorNameToUse的變量賦予一個(gè)字符串類(lèi)型初始值。由于userDefinedColorName值為空萤彩,因此表達(dá)式userDefinedColorName ?? defaultColorName返回defaultColorName的值粪滤,即red。
另一種情況雀扶,分配一個(gè)非空值(non-nil)給userDefinedColorName杖小,再次執(zhí)行空合運(yùn)算,運(yùn)算結(jié)果為封包在userDefaultColorName中的值愚墓,而非默認(rèn)值予权。
userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName非空,因此colorNameToUse的值為"green"
7.閉區(qū)間運(yùn)算符
8.邏輯運(yùn)算符組合計(jì)算加括號(hào)更清晰
9.字符串是值類(lèi)型(Strings Are Value Types)
10.使用字符(Working with Characters)let catCharacters: [Character] = ["C", "a", "t", "!", "??"]
11.連接字符串和字符(Concatenating Strings and Characters)—->>+
12.字符串插值(String Interpolation)
13.字符串字面量的特殊字符(Special Characters in String Literals)
轉(zhuǎn)義字符\0(空字符)浪册、\\(反斜線)扫腺、\t(水平制表符)、\n(換行符)村象、\r(回車(chē)符)笆环、\"(雙引號(hào))、\'(單引號(hào))厚者。
14.字符串索引(String Indices)
15.數(shù)組(Arrays)是有序數(shù)據(jù)的集躁劣。集合(Sets)是無(wú)序無(wú)重復(fù)數(shù)據(jù)的集。字典(Dictionaries)是無(wú)序的鍵值對(duì)的集库菲。
16.訪問(wèn)和修改數(shù)組shoppingList[4...6] = ["Bananas", "Apples"]
17.遍歷:同時(shí)需要每個(gè)數(shù)據(jù)項(xiàng)的值和索引值账忘,可以使用enumerate()方法來(lái)進(jìn)行數(shù)組遍歷
集合(Sets)
集合(Set)用來(lái)存儲(chǔ)相同類(lèi)型并且沒(méi)有確定順序的值。當(dāng)集合元素順序不重要時(shí)或者希望確保每個(gè)元素只出現(xiàn)一次時(shí)可以使用集合而不是數(shù)組蝙昙。
注意:
Swift的Set類(lèi)型被橋接到Foundation中的NSSet類(lèi)闪萄。
18.集合類(lèi)型語(yǔ)法
Swift中的Set類(lèi)型被寫(xiě)為Set,這里的Element表示Set中允許存儲(chǔ)的類(lèi)型奇颠,和數(shù)組不同的是败去,集合沒(méi)有等價(jià)的簡(jiǎn)化形式。
19.創(chuàng)建和構(gòu)造一個(gè)空的集合var letters = Set()
20.用數(shù)組字面量創(chuàng)建集合var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
21.創(chuàng)建一個(gè)空字典var namesOfIntegers = [Int: String]()
22.namesOfIntegers[16] = "sixteen"
// namesOfIntegers現(xiàn)在包含一個(gè)鍵值對(duì)
namesOfIntegers = [:]
// namesOfIntegers又成為了一個(gè)[Int: String]類(lèi)型的空字典
如果上下文已經(jīng)提供了類(lèi)型信息烈拒,我們可以使用空字典字面量來(lái)創(chuàng)建一個(gè)空字典,記作[:](中括號(hào)中放一個(gè)冒號(hào)):
23.用字典字面量創(chuàng)建字典var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
24.訪問(wèn)和修改字典在字典中使用下標(biāo)語(yǔ)法來(lái)添加新的數(shù)據(jù)項(xiàng)airports["LHR"] = "London"
25.While循環(huán)
元組(Tuple)
我們可以使用元組在同一個(gè)switch語(yǔ)句中測(cè)試多個(gè)值砚哆。元組中的元素可以是值,也可以是區(qū)間屑墨。另外躁锁,使用下劃線(_)來(lái)匹配所有可能的值损趋。
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("(0, 0) is at the origin")
case (_, 0):
print("(\(somePoint.0), 0) is on the x-axis")
case (0, _):
print("(0, \(somePoint.1)) is on the y-axis")
case (-2...2, -2...2):
print("(\(somePoint.0), \(somePoint.1)) is inside the box")
default:
print("(\(somePoint.0), \(somePoint.1)) is outside of the box")
}
//輸出"(1, 1) is inside the box"
Where
case分支的模式可以使用where語(yǔ)句來(lái)判斷額外的條件串塑。
26.控制轉(zhuǎn)移語(yǔ)句(Control Transfer Statements)
控制轉(zhuǎn)移語(yǔ)句改變你代碼的執(zhí)行順序,通過(guò)它你可以實(shí)現(xiàn)代碼的跳轉(zhuǎn)予弧。Swift有五種控制轉(zhuǎn)移語(yǔ)句:
continue
break
fallthrough
return
throw
27.帶標(biāo)簽的語(yǔ)句
提前退出
像if語(yǔ)句一樣以躯,guard的執(zhí)行取決于一個(gè)表達(dá)式的布爾值槐秧。我們可以使用guard語(yǔ)句來(lái)要求條件必須為真時(shí),以執(zhí)行g(shù)uard語(yǔ)句后的代碼忧设。不同于if語(yǔ)句刁标,一個(gè)guard語(yǔ)句總是有一個(gè)else分句,如果條件不為真則執(zhí)行else分句中的代碼址晕。
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(["name": "John"])
//輸出"Hello John!"
//輸出"I hope the weather is nice near you."
greet(["name": "Jane", "location": "Cupertino"])
//輸出"Hello Jane!"
//輸出"I hope the weather is nice in Cupertino."
28.函數(shù)(Functions)
函數(shù)定義與調(diào)用(Defining and Calling Functions)
函數(shù)參數(shù)與返回值(Function Parameters and Return Values)
函數(shù)參數(shù)名稱(chēng)(Function Parameter Names)
函數(shù)類(lèi)型(Function Types)
無(wú)返回值函數(shù)(Functions Without Return Values)
嚴(yán)格上來(lái)說(shuō)膀懈,雖然沒(méi)有返回值被定義,sayGoodbye(_:)函數(shù)依然返回了值谨垃。沒(méi)有定義返回類(lèi)型的函數(shù)會(huì)返回特殊的值启搂,叫Void硼控。它其實(shí)是一個(gè)空的元組(tuple),沒(méi)有任何元素胳赌,可以寫(xiě)成()牢撼。
29.多重返回值函數(shù)(Functions with Multiple Return Values)
你可以用元組(tuple)類(lèi)型讓多個(gè)值作為一個(gè)復(fù)合值從函數(shù)中返回。
30.可選元組返回類(lèi)型(Optional Tuple Return Types)
如果函數(shù)返回的元組類(lèi)型有可能整個(gè)元組都“沒(méi)有值”疑苫,你可以使用可選的(Optional)元組返回類(lèi)型反映整個(gè)元組可以是nil的事實(shí)熏版。你可以通過(guò)在元組類(lèi)型的右括號(hào)后放置一個(gè)問(wèn)號(hào)來(lái)定義一個(gè)可選元組,例如(Int, Int)?或(String, Int, Bool)?
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
31.函數(shù)參數(shù)名稱(chēng)(Function Parameter Names)
一般情況下缀匕,第一個(gè)參數(shù)省略其外部參數(shù)名纳决,第二個(gè)以及隨后的參數(shù)使用其局部參數(shù)名作為外部參數(shù)名。所有參數(shù)必須有獨(dú)一無(wú)二的局部參數(shù)名乡小。盡管多個(gè)參數(shù)可以有相同的外部參數(shù)名,但不同的外部參數(shù)名能讓你的代碼更有可讀性饵史。
32.指定外部參數(shù)名(Specifying External Parameter Names)
你可以在局部參數(shù)名前指定外部參數(shù)名满钟,中間以空格分隔:
func someFunction(externalParameterName localParameterName: Int) {
// function body goes here, and can use localParameterName
// to refer to the argument value for that parameter
}
注意
如果你提供了外部參數(shù)名,那么函數(shù)在被調(diào)用時(shí)胳喷,必須使用外部參數(shù)名湃番。
這個(gè)版本的sayHello(_:)函數(shù),接收兩個(gè)人的名字吭露,會(huì)同時(shí)返回對(duì)他倆的問(wèn)候:
func sayHello(to person: String, and anotherPerson: String) -> String {
return "Hello \(person) and \(anotherPerson)!"
}
print(sayHello(to: "Bill", and: "Ted"))
// prints "Hello Bill and Ted!"
為每個(gè)參數(shù)指定外部參數(shù)名后吠撮,在你調(diào)用sayHello(to:and:)函數(shù)時(shí)兩個(gè)外部參數(shù)名都必須寫(xiě)出來(lái)。
使用外部函數(shù)名可以使函數(shù)以一種更富有表達(dá)性的類(lèi)似句子的方式調(diào)用讲竿,并使函數(shù)體意圖清晰泥兰,更具可讀性。
33.忽略外部參數(shù)名(Omitting External Parameter Names)
如果你不想為第二個(gè)及后續(xù)的參數(shù)設(shè)置外部參數(shù)名题禀,用一個(gè)下劃線(_)代替一個(gè)明確的參數(shù)名鞋诗。
func someFunction(firstParameterName: Int, _ secondParameterName: Int) {
// function body goes here
// firstParameterName and secondParameterName refer to
// the argument values for the first and second parameters
}
someFunction(1, 2)
注意
因?yàn)榈谝粋€(gè)參數(shù)默認(rèn)忽略其外部參數(shù)名稱(chēng),顯式地寫(xiě)下劃線是多余的迈嘹。
34.默認(rèn)參數(shù)值(Default Parameter Values)
你可以在函數(shù)體中為每個(gè)參數(shù)定義默認(rèn)值(Deafult Values)削彬。當(dāng)默認(rèn)值被定義后,調(diào)用這個(gè)函數(shù)時(shí)可以忽略這個(gè)參數(shù)秀仲。
35.可變參數(shù)(Variadic Parameters)
一個(gè)可變參數(shù)(variadic parameter)可以接受零個(gè)或多個(gè)值融痛。函數(shù)調(diào)用時(shí),你可以用可變參數(shù)來(lái)指定函數(shù)參數(shù)可以被傳入不確定數(shù)量的輸入值神僵。通過(guò)在變量類(lèi)型名后面加入(...)的方式來(lái)定義可變參數(shù)雁刷。
可變參數(shù)的傳入值在函數(shù)體中變?yōu)榇祟?lèi)型的一個(gè)數(shù)組。例如挑豌,一個(gè)叫做numbers的Double...型可變參數(shù)安券,在函數(shù)體內(nèi)可以當(dāng)做一個(gè)叫numbers的[Double]型的數(shù)組常量墩崩。
下面的這個(gè)函數(shù)用來(lái)計(jì)算一組任意長(zhǎng)度數(shù)字的算術(shù)平均數(shù)(arithmetic mean):
func arithmeticMean(numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers
注意
一個(gè)函數(shù)最多只能有一個(gè)可變參數(shù)。
35.常量參數(shù)和變量參數(shù)(Constant and Variable Parameters)
函數(shù)參數(shù)默認(rèn)是常量侯勉。試圖在函數(shù)體中更改參數(shù)值將會(huì)導(dǎo)致編譯錯(cuò)誤鹦筹。這意味著你不能錯(cuò)誤地更改參數(shù)值。
但是址貌,有時(shí)候铐拐,如果函數(shù)中有傳入?yún)?shù)的變量值副本將是很有用的。你可以通過(guò)指定一個(gè)或多個(gè)參數(shù)為變量參數(shù)练对,從而避免自己在函數(shù)中定義新的變量酿联。變量參數(shù)不是常量,你可以在函數(shù)中把它當(dāng)做新的可修改副本來(lái)使用升熊。
通過(guò)在參數(shù)名前加關(guān)鍵字var來(lái)定義變量參數(shù):
func alignRight(var string: String, totalLength: Int, pad: Character) -> String {
let amountToPad = totalLength - string.characters.count
if amountToPad < 1 {
return string
}
let padString = String(pad)
for _ in 1...amountToPad {
string = padString + string
}
return string
}
let originalString = "hello"
let paddedString = alignRight(originalString, totalLength: 10, pad: "-")
// paddedString is equal to "-----hello"
// originalString is still equal to "hello"
這個(gè)例子中定義了一個(gè)叫做alignRight(_:totalLength:pad:)的新函數(shù)菱涤,用來(lái)將輸入的字符串對(duì)齊到更長(zhǎng)的輸出字符串的右邊緣。左側(cè)空余的地方用指定的填充字符填充螺男。這個(gè)例子中棒厘,字符串"hello"被轉(zhuǎn)換成了"-----hello"。
alignRight(_:totalLength:pad:)函數(shù)將輸入?yún)?shù)string定義為變量參數(shù)下隧。這意味著string現(xiàn)在可以作為一個(gè)局部變量奢人,被傳入的字符串值初始化,并且可以在函數(shù)體中進(jìn)行操作淆院。
函數(shù)首先計(jì)算出有多少字符需要被添加到string的左邊何乎,從而將其在整個(gè)字符串中右對(duì)齊。這個(gè)值存儲(chǔ)在一個(gè)稱(chēng)為amountToPad的本地常量土辩。如果不需要填充(也就是說(shuō)支救,如果amountToPad小于1),該函數(shù)簡(jiǎn)單地返回沒(méi)有任何填充的輸入值string脯燃。
否則搂妻,該函數(shù)用pad字符創(chuàng)建一個(gè)叫做padString的臨時(shí)String常量,并將amountToPad個(gè)padString添加到現(xiàn)有字符串的左邊辕棚。(一個(gè)String值不能被添加到一個(gè)Character值上欲主,所以padString常量用于確保+操作符兩側(cè)都是String值)。
注意
對(duì)變量參數(shù)所進(jìn)行的修改在函數(shù)調(diào)用結(jié)束后便消失了逝嚎,并且對(duì)于函數(shù)體外是不可見(jiàn)的扁瓢。變量參數(shù)僅僅存在于函數(shù)調(diào)用的生命周期中。
36.輸入輸出參數(shù)(In-Out Parameters)
變量參數(shù)补君,正如上面所述引几,僅僅能在函數(shù)體內(nèi)被更改。如果你想要一個(gè)函數(shù)可以修改參數(shù)的值,并且想要在這些修改在函數(shù)調(diào)用結(jié)束后仍然存在伟桅,那么就應(yīng)該把這個(gè)參數(shù)定義為輸入輸出參數(shù)(In-Out Parameters)敞掘。
定義一個(gè)輸入輸出參數(shù)時(shí),在參數(shù)定義前加inout關(guān)鍵字楣铁。一個(gè)輸入輸出參數(shù)有傳入函數(shù)的值玖雁,這個(gè)值被函數(shù)修改,然后被傳出函數(shù)盖腕,替換原來(lái)的值赫冬。想獲取更多的關(guān)于輸入輸出參數(shù)的細(xì)節(jié)和相關(guān)的編譯器優(yōu)化,請(qǐng)查看輸入輸出參數(shù)一節(jié)溃列。
你只能傳遞變量給輸入輸出參數(shù)劲厌。你不能傳入常量或者字面量(literal value),因?yàn)檫@些量是不能被修改的听隐。當(dāng)傳入的參數(shù)作為輸入輸出參數(shù)時(shí)补鼻,需要在參數(shù)名前加&符,表示這個(gè)值可以被函數(shù)修改雅任。
注意
輸入輸出參數(shù)不能有默認(rèn)值辽幌,而且可變參數(shù)不能用inout標(biāo)記。如果你用inout標(biāo)記一個(gè)參數(shù)椿访,這個(gè)參數(shù)不能被var或者let標(biāo)記。
下面是例子虑润,swapTwoInts(_:_:)函數(shù)成玫,有兩個(gè)分別叫做a和b的輸入輸出參數(shù):
func swapTwoInts(inout a: Int, inout _ b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
這個(gè)swapTwoInts(_:_:)函數(shù)簡(jiǎn)單地交換a與b的值。該函數(shù)先將a的值存到一個(gè)臨時(shí)常量temporaryA中拳喻,然后將b的值賦給a哭当,最后將temporaryA賦值給b。
你可以用兩個(gè)Int型的變量來(lái)調(diào)用swapTwoInts(_:_:)冗澈。需要注意的是钦勘,someInt和anotherInt在傳入swapTwoInts(_:_:)函數(shù)前,都加了&的前綴:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"
從上面這個(gè)例子中亚亲,我們可以看到someInt和anotherInt的原始值在swapTwoInts(_:_:)函數(shù)中被修改彻采,盡管它們的定義在函數(shù)體外。
注意
輸入輸出參數(shù)和返回值是不一樣的捌归。上面的swapTwoInts函數(shù)并沒(méi)有定義任何返回值肛响,但仍然修改了someInt和anotherInt的值。輸入輸出參數(shù)是函數(shù)對(duì)函數(shù)體外產(chǎn)生影響的另一種方式惜索。
37.使用函數(shù)類(lèi)型(Using Function Types)
在Swift中嘹屯,使用函數(shù)類(lèi)型就像使用其他類(lèi)型一樣瞬哼。例如疙剑,你可以定義一個(gè)類(lèi)型為函數(shù)的常量或變量宰闰,并將適當(dāng)?shù)暮瘮?shù)賦值給它:
var mathFunction: (Int, Int) -> Int = addTwoInts
這個(gè)可以解讀為:
“定義一個(gè)叫做mathFunction的變量,類(lèi)型是‘一個(gè)有兩個(gè)Int型的參數(shù)并返回一個(gè)Int型的值的函數(shù)’缸榄,并讓這個(gè)新變量指向addTwoInts函數(shù)”。
addTwoInts和mathFunction有同樣的類(lèi)型,所以這個(gè)賦值過(guò)程在Swift類(lèi)型檢查中是允許的淘讥。
現(xiàn)在,你可以用mathFunction來(lái)調(diào)用被賦值的函數(shù)了:
print("Result: \(mathFunction(2, 3))")
// prints "Result: 5"
有相同匹配類(lèi)型的不同函數(shù)可以被賦值給同一個(gè)變量质帅,就像非函數(shù)類(lèi)型的變量一樣:
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// prints "Result: 6"
就像其他類(lèi)型一樣适揉,當(dāng)賦值一個(gè)函數(shù)給常量或變量時(shí),你可以讓Swift來(lái)推斷其函數(shù)類(lèi)型:
let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int
38.函數(shù)類(lèi)型作為參數(shù)類(lèi)型(Function Types as Parameter Types)
你可以用(Int, Int) -> Int這樣的函數(shù)類(lèi)型作為另一個(gè)函數(shù)的參數(shù)類(lèi)型煤惩。這樣你可以將函數(shù)的一部分實(shí)現(xiàn)留給函數(shù)的調(diào)用者來(lái)提供嫉嘀。
下面是另一個(gè)例子,正如上面的函數(shù)一樣魄揉,同樣是輸出某種數(shù)學(xué)運(yùn)算結(jié)果:
func printMathResult(mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// prints "Result: 8"
printMathResult(_:_:_:)函數(shù)的作用就是輸出另一個(gè)適當(dāng)類(lèi)型的數(shù)學(xué)函數(shù)的調(diào)用結(jié)果剪侮。它不關(guān)心傳入函數(shù)是如何實(shí)現(xiàn)的,它只關(guān)心這個(gè)傳入的函數(shù)類(lèi)型是正確的洛退。這使得printMathResult(_:_:_:)能以一種類(lèi)型安全(type-safe)的方式將一部分功能轉(zhuǎn)給調(diào)用者實(shí)現(xiàn)瓣俯。
39.函數(shù)類(lèi)型作為返回類(lèi)型(Function Types as Return Types)
你可以用函數(shù)類(lèi)型作為另一個(gè)函數(shù)的返回類(lèi)型。你需要做的是在返回箭頭(->)后寫(xiě)一個(gè)完整的函數(shù)類(lèi)型兵怯。
func stepForward(input: Int) -> Int {
return input + 1
}
func stepBackward(input: Int) -> Int {
return input - 1
}
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
return backwards ? stepBackward : stepForward
}
你現(xiàn)在可以用chooseStepFunction(_:)來(lái)獲得兩個(gè)函數(shù)其中的一個(gè):
var currentValue = 3
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function
一個(gè)指向返回的函數(shù)的引用保存在了moveNearerToZero常量中彩匕。
40.嵌套函數(shù)(Nested Functions)
這章中你所見(jiàn)到的所有函數(shù)都叫全局函數(shù)(global functions),它們定義在全局域中媒区。你也可以把函數(shù)定義在別的函數(shù)體中驼仪,稱(chēng)作嵌套函數(shù)(nested functions)。
默認(rèn)情況下袜漩,嵌套函數(shù)是對(duì)外界不可見(jiàn)的绪爸,但是可以被它們的外圍函數(shù)(enclosing function)調(diào)用。一個(gè)外圍函數(shù)也可以返回它的某一個(gè)嵌套函數(shù)宙攻,使得這個(gè)函數(shù)可以在其他域中被使用奠货。
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backwards ? stepBackward : stepForward
}
41.閉包表達(dá)式語(yǔ)法(Closure Expression Syntax)
閉包表達(dá)式語(yǔ)法有如下一般形式:
{ (parameters) -> returnType in
statements
}
閉包表達(dá)式語(yǔ)法可以使用常量、變量和inout類(lèi)型作為參數(shù)座掘,不能提供默認(rèn)值递惋。也可以在參數(shù)列表的最后使用可變參數(shù)。元組也可以作為參數(shù)和返回值雹顺。
下面的例子展示了之前backwards(_:_:)函數(shù)對(duì)應(yīng)的閉包表達(dá)式版本的代碼:
reversed = names.sort({ (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是內(nèi)聯(lián)閉包參數(shù)和返回值類(lèi)型聲明與backwards(_:_:)函數(shù)類(lèi)型聲明相同丹墨。在這兩種方式中,都寫(xiě)成了(s1: String, s2: String) -> Bool嬉愧。然而在內(nèi)聯(lián)閉包表達(dá)式中贩挣,函數(shù)和返回值類(lèi)型都寫(xiě)在大括號(hào)內(nèi),而不是大括號(hào)外。
閉包的函數(shù)體部分由關(guān)鍵字in引入王财。該關(guān)鍵字表示閉包的參數(shù)和返回值類(lèi)型定義已經(jīng)完成卵迂,閉包函數(shù)體即將開(kāi)始。
由于這個(gè)閉包的函數(shù)體部分如此短绒净,以至于可以將其改寫(xiě)成一行代碼:
reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )
該例中sort(_:)方法的整體調(diào)用保持不變见咒,一對(duì)圓括號(hào)仍然包裹住了方法的整個(gè)參數(shù)。然而挂疆,參數(shù)現(xiàn)在變成了內(nèi)聯(lián)閉包改览。
42.根據(jù)上下文推斷類(lèi)型(Inferring Type From Context)
因?yàn)榕判蜷]包函數(shù)是作為sort(_:)方法的參數(shù)傳入的,Swift可以推斷其參數(shù)和返回值的類(lèi)型缤言。sort(_:)方法被一個(gè)字符串?dāng)?shù)組調(diào)用宝当,因此其參數(shù)必須是(String, String) -> Bool類(lèi)型的函數(shù)。這意味著(String, String)和Bool類(lèi)型并不需要作為閉包表達(dá)式定義的一部分胆萧。因?yàn)樗械念?lèi)型都可以被正確推斷庆揩,返回箭頭(->)和圍繞在參數(shù)周?chē)睦ㄌ?hào)也可以被省略:
reversed = names.sort( { s1, s2 in return s1 > s2 } )
實(shí)際上任何情況下,通過(guò)內(nèi)聯(lián)閉包表達(dá)式構(gòu)造的閉包作為參數(shù)傳遞給函數(shù)或方法時(shí)跌穗,都可以推斷出閉包的參數(shù)和返回值類(lèi)型订晌。這意味著閉包作為函數(shù)或者方法的參數(shù)時(shí),您幾乎不需要利用完整格式構(gòu)造內(nèi)聯(lián)閉包蚌吸。
盡管如此锈拨,您仍然可以明確寫(xiě)出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性羹唠,則可以采用完整格式的閉包推励。而在sort(_:)方法這個(gè)例子里,閉包的目的就是排序肉迫。由于這個(gè)閉包是為了處理字符串?dāng)?shù)組的排序,因此讀者能夠推測(cè)出這個(gè)閉包是用于字符串處理的稿黄。
43.參數(shù)名稱(chēng)縮寫(xiě)(Shorthand Argument Names)
Swift自動(dòng)為內(nèi)聯(lián)閉包提供了參數(shù)名稱(chēng)縮寫(xiě)功能喊衫,您可以直接通過(guò)$0,$1杆怕,$2來(lái)順序調(diào)用閉包的參數(shù)族购,以此類(lèi)推。
如果您在閉包表達(dá)式中使用參數(shù)名稱(chēng)縮寫(xiě)陵珍,您可以在閉包參數(shù)列表中省略對(duì)其的定義,并且對(duì)應(yīng)參數(shù)名稱(chēng)縮寫(xiě)的類(lèi)型會(huì)通過(guò)函數(shù)類(lèi)型進(jìn)行推斷。in關(guān)鍵字也同樣可以被省略惠况,因?yàn)榇藭r(shí)閉包表達(dá)式完全由閉包函數(shù)體構(gòu)成:
reversed = names.sort( { $0 > $1 } )
在這個(gè)例子中迎变,$0和$1表示閉包中第一個(gè)和第二個(gè)String類(lèi)型的參數(shù)。
44.運(yùn)算符函數(shù)(Operator Functions)
實(shí)際上還有一種更簡(jiǎn)短的方式來(lái)撰寫(xiě)上面例子中的閉包表達(dá)式。Swift的String類(lèi)型定義了關(guān)于大于號(hào)(>)的字符串實(shí)現(xiàn)只盹,其作為一個(gè)函數(shù)接受兩個(gè)String類(lèi)型的參數(shù)并返回Bool類(lèi)型的值辣往。而這正好與sort(_:)方法的第二個(gè)參數(shù)需要的函數(shù)類(lèi)型相符合。因此殖卑,您可以簡(jiǎn)單地傳遞一個(gè)大于號(hào)站削,Swift可以自動(dòng)推斷出您想使用大于號(hào)的字符串函數(shù)實(shí)現(xiàn):
reversed = names.sort(>)
45.尾隨閉包(Trailing Closures)
如果您需要將一個(gè)很長(zhǎng)的閉包表達(dá)式作為最后一個(gè)參數(shù)傳遞給函數(shù),可以使用尾隨閉包來(lái)增強(qiáng)函數(shù)的可讀性孵稽。尾隨閉包是一個(gè)書(shū)寫(xiě)在函數(shù)括號(hào)之后的閉包表達(dá)式许起,函數(shù)支持將其作為最后一個(gè)參數(shù)調(diào)用:
func someFunctionThatTakesAClosure(closure: () -> Void) {
//函數(shù)體部分
}
//以下是不使用尾隨閉包進(jìn)行函數(shù)調(diào)用
someFunctionThatTakesAClosure({
//閉包主體部分
})
//以下是使用尾隨閉包進(jìn)行函數(shù)調(diào)用
someFunctionThatTakesAClosure() {
//閉包主體部分
}
在閉包表達(dá)式語(yǔ)法一節(jié)中作為sort(_:)方法參數(shù)的字符串排序閉包可以改寫(xiě)為:
reversed = names.sort() { $0 > $1 }
如果函數(shù)只需要閉包表達(dá)式一個(gè)參數(shù),當(dāng)您使用尾隨閉包時(shí)菩鲜,您甚至可以把()省略掉:
reversed = names.sort { $0 > $1 }
let digitNames = [
0: "Zero", 1: "One", 2: "Two",3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
如上代碼創(chuàng)建了一個(gè)數(shù)字位和它們英文版本名字相映射的字典园细。同時(shí)還定義了一個(gè)準(zhǔn)備轉(zhuǎn)換為字符串?dāng)?shù)組的整型數(shù)組。
您現(xiàn)在可以通過(guò)傳遞一個(gè)尾隨閉包給numbers的map(_:)方法來(lái)創(chuàng)建對(duì)應(yīng)的字符串版本數(shù)組:
let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings常量被推斷為字符串類(lèi)型數(shù)組睦袖,即[String]
//其值為["OneSix", "FiveEight", "FiveOneZero"]
46.解決閉包引起的循環(huán)強(qiáng)引用:解決閉包引起的循環(huán)強(qiáng)引用
定義捕獲列表
捕獲列表中的每一項(xiàng)都由一對(duì)元素組成珊肃,一個(gè)元素是weak或unowned關(guān)鍵字,另一個(gè)元素是類(lèi)實(shí)例的引用(例如self)或初始化過(guò)的變量(如delegate = self.delegate!)馅笙。這些項(xiàng)在方括號(hào)中用逗號(hào)分開(kāi)伦乔。
如果閉包有參數(shù)列表和返回類(lèi)型,把捕獲列表放在它們前面:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
//這里是閉包的函數(shù)體
}
如果閉包沒(méi)有指明參數(shù)列表或者返回類(lèi)型董习,即它們會(huì)通過(guò)上下文推斷烈和,那么可以把捕獲列表和關(guān)鍵字in放在閉包最開(kāi)始的地方:
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
//這里是閉包的函數(shù)體
}
弱引用和無(wú)主引用
47.
在閉包和捕獲的實(shí)例總是互相引用并且總是同時(shí)銷(xiāo)毀時(shí),將閉包內(nèi)的捕獲定義為無(wú)主引用皿淋。
相反的招刹,在被捕獲的引用可能會(huì)變?yōu)?/b>nil時(shí),將閉包內(nèi)的捕獲定義為弱引用窝趣。弱引用總是可選類(lèi)型疯暑,并且當(dāng)引用的實(shí)例被銷(xiāo)毀后,弱引用的值會(huì)自動(dòng)置為nil哑舒。這使我們可以在閉包體內(nèi)檢查它們是否存在妇拯。
注意
如果被捕獲的引用絕對(duì)不會(huì)變?yōu)?/b>nil,應(yīng)該用無(wú)主引用洗鸵,而不是弱引用越锈。
前面的HTMLElement例子中,無(wú)主引用是正確的解決循環(huán)強(qiáng)引用的方法膘滨。這樣編寫(xiě)HTMLElement類(lèi)來(lái)避免循環(huán)強(qiáng)引用:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
上面的HTMLElement實(shí)現(xiàn)和之前的實(shí)現(xiàn)一致甘凭,除了在asHTML閉包中多了一個(gè)捕獲列表。這里火邓,捕獲列表是[unowned self]丹弱,表示“將self捕獲為無(wú)主引用而不是強(qiáng)引用”德撬。
和之前一樣,我們可以創(chuàng)建并打印HTMLElement實(shí)例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
//打印“
hello, world
”
使用捕獲列表后引用關(guān)系如下圖所示:
這一次蹈矮,閉包以無(wú)主引用的形式捕獲self砰逻,并不會(huì)持有HTMLElement實(shí)例的強(qiáng)引用。如果將paragraph賦值為nil泛鸟,HTMLElement實(shí)例將會(huì)被銷(xiāo)毀蝠咆,并能看到它的析構(gòu)函數(shù)打印出的消息:
paragraph = nil
//打印“p is being deinitialized”
48.閉包是引用類(lèi)型(Closures Are Reference Types)
上面的例子中,incrementBySeven和incrementByTen是常量北滥,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值刚操。這是因?yàn)楹瘮?shù)和閉包都是引用類(lèi)型。
無(wú)論您將函數(shù)或閉包賦值給一個(gè)常量還是變量再芋,您實(shí)際上都是將常量或變量的值設(shè)置為對(duì)應(yīng)函數(shù)或閉包的引用菊霜。上面的例子中,指向閉包的引用incrementByTen是一個(gè)常量济赎,而并非閉包內(nèi)容本身鉴逞。
這也意味著如果您將閉包賦值給了兩個(gè)不同的常量或變量,兩個(gè)值都會(huì)指向同一個(gè)閉包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
//返回的值為50
49.枚舉語(yǔ)法
使用enum關(guān)鍵詞來(lái)創(chuàng)建枚舉并且把它們的整個(gè)定義放在一對(duì)大括號(hào)內(nèi):
enum CompassPoint {
case North
case South
case East
case West
}
與C和Objective-C不同司训,Swift的枚舉成員在被創(chuàng)建時(shí)不會(huì)被賦予一個(gè)默認(rèn)的整型值。在上面的CompassPoint例子中喘帚,North,South咒钟,East和West不會(huì)被隱式地賦值為0吹由,1,2和3朱嘴。相反腕够,這些枚舉成員本身就是完備的值焙贷,這些值的類(lèi)型是已經(jīng)明確定義好的CompassPoint類(lèi)型。
多個(gè)成員值可以出現(xiàn)在同一行上趣效,用逗號(hào)隔開(kāi):
enum Planet {
case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
每個(gè)枚舉定義了一個(gè)全新的類(lèi)型鬼廓。像Swift中其他類(lèi)型一樣党巾,它們的名字(例如CompassPoint和Planet)應(yīng)該以一個(gè)大寫(xiě)字母開(kāi)頭。給枚舉類(lèi)型起一個(gè)單數(shù)名字而不是復(fù)數(shù)名字,以便于讀起來(lái)更加
容易理解:
var directionToHead = CompassPoint.West
directionToHead的類(lèi)型可以在它被CompassPoint的某個(gè)值初始化時(shí)推斷出來(lái)芽隆。一旦directionToHead
被聲明為CompassPoint類(lèi)型窿侈,你可以使用更簡(jiǎn)短的點(diǎn)語(yǔ)法將其設(shè)置為另一個(gè)CompassPoint的值:
directionToHead = .East
當(dāng)directionToHead的類(lèi)型已知時(shí)衙傀,再次為其賦值可以省略枚舉類(lèi)型名。在使用具有顯式類(lèi)型的枚舉值時(shí),這種寫(xiě)法讓代碼具有更好的可讀性迷雪。
switch語(yǔ)句必須窮舉所有情況忆谓。如果忽略了.West這種情況采桃,上面那段代碼將無(wú)法通過(guò)編譯,因?yàn)樗鼪](méi)有考慮到CompassPoint的全部成員蓝仲。強(qiáng)制窮舉確保了枚舉成員不會(huì)被意外遺漏犀盟。
當(dāng)不需要匹配每個(gè)枚舉成員的時(shí)候血公,你可以提供一個(gè)default分支來(lái)涵蓋所有未明確處理的枚舉成員:
let somePlanet = Planet.Earth
switch somePlanet {
case .Earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
//輸出"Mostly harmless”
50.關(guān)聯(lián)值(Associated Values)
在Swift中况毅,使用如下方式定義表示兩種商品條形碼的枚舉:
enum Barcode {
case UPCA(Int, Int, Int, Int)
case QRCode(String)
}
“定義一個(gè)名為Barcode的枚舉類(lèi)型窍荧,它的一個(gè)成員值是具有(Int,Int,Int,Int)類(lèi)型關(guān)聯(lián)值的UPCA,另一個(gè)成員值是具有String類(lèi)型關(guān)聯(lián)值的QRCode。”
這個(gè)定義不提供任何Int或String類(lèi)型的關(guān)聯(lián)值真竖,它只是定義了恢共,當(dāng)Barcode常量和變量等于Barcode.UPCA或Barcode.QRCode時(shí),可以存儲(chǔ)的關(guān)聯(lián)值的類(lèi)型。
switch productBarcode {
case .UPCA(let numberSystem, let manufacturer, let product, let check):
print("UPC-A: \(numberSystem), \(manufacturer), \(product), \(check).")
case .QRCode(let productCode):
print("QR code: \(productCode).")
}
//輸出"QR code: ABCDEFGHIJKLMNOP."
如果一個(gè)枚舉成員的所有關(guān)聯(lián)值都被提取為常量跨新,或者都被提取為變量含懊,為了簡(jiǎn)潔,你可以只在成員名稱(chēng)前標(biāo)注一個(gè)let或者var:
switch productBarcode {
case let .UPCA(numberSystem, manufacturer, product, check):
print("UPC-A: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .QRCode(productCode):
print("QR code: \(productCode).")
}
//輸出"QR code: ABCDEFGHIJKLMNOP."
51.原始值(Raw Values)
在關(guān)聯(lián)值小節(jié)的條形碼例子中宵膨,演示了如何聲明存儲(chǔ)不同類(lèi)型關(guān)聯(lián)值的枚舉成員灭将。作為關(guān)聯(lián)值的替代選擇问窃,枚舉成員可以被默認(rèn)值(稱(chēng)為原始值)預(yù)填充危喉,這些原始值的類(lèi)型必須相同杭措。
這是一個(gè)使用ASCII碼作為原始值的枚舉:
enum ASCIIControlCharacter: Character {
case Tab = "\t"
case LineFeed = "\n"
case CarriageReturn = "\r"
}
枚舉類(lèi)型ASCIIControlCharacter的原始值類(lèi)型被定義為Character费什,并設(shè)置了一些比較常見(jiàn)的ASCII控制字符。Character的描述詳見(jiàn)字符串和字符部分手素。
原始值可以是字符串鸳址,字符,或者任意整型值或浮點(diǎn)型值泉懦。每個(gè)原始值在枚舉聲明中必須是唯一的稿黍。
注意
原始值和關(guān)聯(lián)值是不同的。原始值是在定義枚舉時(shí)被預(yù)先填充的值崩哩,像上述三個(gè)ASCII碼巡球。對(duì)于一個(gè)特定的枚舉成員,它的原始值始終不變邓嘹。關(guān)聯(lián)值是創(chuàng)建一個(gè)基于枚舉成員的常量或變量時(shí)才設(shè)置的值酣栈,枚舉成員的關(guān)聯(lián)值可以變化。
52.原始值的隱式賦值(Implicitly Assigned Raw Values)
在使用原始值為整數(shù)或者字符串類(lèi)型的枚舉時(shí)汹押,不需要顯式地為每一個(gè)枚舉成員設(shè)置原始值矿筝,Swift將會(huì)自動(dòng)為你賦值。
例如棚贾,當(dāng)使用整數(shù)作為原始值時(shí)窖维,隱式賦值的值依次遞增1。如果第一個(gè)枚舉成員沒(méi)有設(shè)置原始值鸟悴,其原始值將為0陈辱。
下面的枚舉是對(duì)之前Planet這個(gè)枚舉的一個(gè)細(xì)化奖年,利用整型的原始值來(lái)表示每個(gè)行星在太陽(yáng)系中的順序:
enum Planet: Int {
case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
在上面的例子中细诸,Plant.Mercury的顯式原始值為1,Planet.Venus的隱式原始值為2陋守,依次類(lèi)推震贵。
當(dāng)使用字符串作為枚舉類(lèi)型的原始值時(shí)利赋,每個(gè)枚舉成員的隱式原始值為該枚舉成員的名稱(chēng)。
下面的例子是CompassPoint枚舉的細(xì)化猩系,使用字符串類(lèi)型的原始值來(lái)表示各個(gè)方向的名稱(chēng):
enum CompassPoint: String {
case North, South, East, West
}
上面例子中媚送,CompassPoint.South擁有隱式原始值South,依次類(lèi)推寇甸。
使用枚舉成員的rawValue屬性可以訪問(wèn)該枚舉成員的原始值:
let earthsOrder = Planet.Earth.rawValue
// earthsOrder值為3
let sunsetDirection = CompassPoint.West.rawValue
// sunsetDirection值為"West"
53.使用原始值初始化枚舉實(shí)例(Initializing from a Raw Value)
如果在定義枚舉類(lèi)型的時(shí)候使用了原始值塘偎,那么將會(huì)自動(dòng)獲得一個(gè)初始化方法,這個(gè)方法接收一個(gè)叫做rawValue的參數(shù)拿霉,參數(shù)類(lèi)型即為原始值類(lèi)型吟秩,返回值則是枚舉成員或nil。你可以使用這個(gè)初始化方法來(lái)創(chuàng)建一個(gè)新的枚舉實(shí)例绽淘。
這個(gè)例子利用原始值7創(chuàng)建了枚舉成員Uranus:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet類(lèi)型為Planet?值為Planet.Uranus
然而涵防,并非所有Int值都可以找到一個(gè)匹配的行星。因此沪铭,原始值構(gòu)造器總是返回一個(gè)可選的枚舉成員壮池。在上面的例子中,possiblePlanet是Planet?類(lèi)型杀怠,或者說(shuō)“可選的Planet”椰憋。
注意
原始值構(gòu)造器是一個(gè)可失敗構(gòu)造器,因?yàn)椴⒉皇敲恳粋€(gè)原始值都有與之對(duì)應(yīng)的枚舉成員赔退。更多信息請(qǐng)參見(jiàn)可失敗構(gòu)造器
如果你試圖尋找一個(gè)位置為9的行星熏矿,通過(guò)原始值構(gòu)造器返回的可選Planet值將是nil:
let positionToFind = 9
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .Earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
//輸出"There isn't a planet at position 9
這個(gè)例子使用了可選綁定(optional binding)访递,試圖通過(guò)原始值9來(lái)訪問(wèn)一個(gè)行星渴频。if let somePlanet = Planet(rawValue: 9)語(yǔ)句創(chuàng)建了一個(gè)可選Planet聋丝,如果可選Planet的值存在奢啥,就會(huì)賦值給somePlanet页响。在這個(gè)例子中垦江,無(wú)法檢索到位置為9的行星侄非,所以else分支被執(zhí)行片拍。
54.遞歸枚舉(Recursive Enumerations)
當(dāng)各種可能的情況可以被窮舉時(shí)浪读,非常適合使用枚舉進(jìn)行數(shù)據(jù)建模昔榴,例如可以用枚舉來(lái)表示用于簡(jiǎn)單整數(shù)運(yùn)算的操作符。這些操作符讓你可以將簡(jiǎn)單的算術(shù)表達(dá)式碘橘,例如整數(shù)5互订,結(jié)合為更為復(fù)雜的表達(dá)式,例如5 + 4痘拆。
算術(shù)表達(dá)式的一個(gè)重要特性是仰禽,表達(dá)式可以嵌套使用。例如,表達(dá)式(5 + 4) * 2吐葵,乘號(hào)右邊是一個(gè)數(shù)字规揪,左邊則是另一個(gè)表達(dá)式。因?yàn)閿?shù)據(jù)是嵌套的温峭,因而用來(lái)存儲(chǔ)數(shù)據(jù)的枚舉類(lèi)型也需要支持這種嵌套——這意味著枚舉類(lèi)型需要支持遞歸猛铅。
遞歸枚舉(recursive enumeration)是一種枚舉類(lèi)型,它有一個(gè)或多個(gè)枚舉成員使用該枚舉類(lèi)型的實(shí)例作為關(guān)聯(lián)值凤藏。使用遞歸枚舉時(shí)奸忽,編譯器會(huì)插入一個(gè)間接層。你可以在枚舉成員前加上indirect來(lái)表示該成員可遞歸揖庄。
例如月杉,下面的例子中,枚舉類(lèi)型存儲(chǔ)了簡(jiǎn)單的算術(shù)表達(dá)式:
enum ArithmeticExpression {
case Number(Int)
indirect case Addition(ArithmeticExpression, ArithmeticExpression)
indirect case Multiplication(ArithmeticExpression, ArithmeticExpression)
}
你也可以在枚舉類(lèi)型開(kāi)頭加上indirect關(guān)鍵字來(lái)表明它的所有成員都是可遞歸的:
indirect enum ArithmeticExpression {
case Number(Int)
case Addition(ArithmeticExpression, ArithmeticExpression)
case Multiplication(ArithmeticExpression, ArithmeticExpression)
}
上面定義的枚舉類(lèi)型可以存儲(chǔ)三種算術(shù)表達(dá)式:純數(shù)字抠艾、兩個(gè)表達(dá)式相加苛萎、兩個(gè)表達(dá)式相乘。枚舉成員Addition和Multiplication的關(guān)聯(lián)值也是算術(shù)表達(dá)式——這些關(guān)聯(lián)值使得嵌套表達(dá)式成為可能检号。
要操作具有遞歸性質(zhì)的數(shù)據(jù)結(jié)構(gòu)腌歉,使用遞歸函數(shù)是一種直截了當(dāng)?shù)姆绞健@缙肟粒旅媸且粋€(gè)對(duì)算術(shù)表達(dá)式求值的函數(shù):
func evaluate(expression: ArithmeticExpression) -> Int {
switch expression {
case .Number(let value):
return value
case .Addition(let left, let right):
return evaluate(left) + evaluate(right)
case .Multiplication(let left, let right):
return evaluate(left) * evaluate(right)
}
}
//計(jì)算(5 + 4) * 2
let five = ArithmeticExpression.Number(5)
let four = ArithmeticExpression.Number(4)
let sum = ArithmeticExpression.Addition(five, four)
let product = ArithmeticExpression.Multiplication(sum, ArithmeticExpression.Number(2))
print(evaluate(product))
//輸出"18"
該函數(shù)如果遇到純數(shù)字翘盖,就直接返回該數(shù)字的值。如果遇到的是加法或乘法運(yùn)算凹蜂,則分別計(jì)算左邊表達(dá)式和右邊表達(dá)式的值馍驯,然后相加或相乘。
55.類(lèi)和結(jié)構(gòu)體(Classes and Structures)
與其他編程語(yǔ)言所不同的是玛痊,Swift并不要求你為自定義類(lèi)和結(jié)構(gòu)去創(chuàng)建獨(dú)立的接口和實(shí)現(xiàn)文件汰瘫。你所要做的是在一個(gè)單一文件中定義一個(gè)類(lèi)或者結(jié)構(gòu)體,系統(tǒng)將會(huì)自動(dòng)生成面向其它代碼的外部接口擂煞。
56.類(lèi)和結(jié)構(gòu)體對(duì)比
Swift中類(lèi)和結(jié)構(gòu)體有很多共同點(diǎn)混弥。共同處在于:
定義屬性用于存儲(chǔ)值
定義方法用于提供功能
定義附屬腳本用于訪問(wèn)值
定義構(gòu)造器用于生成初始化值
通過(guò)擴(kuò)展以增加默認(rèn)實(shí)現(xiàn)的功能
實(shí)現(xiàn)協(xié)議以提供某種標(biāo)準(zhǔn)功能
與結(jié)構(gòu)體相比,類(lèi)還有如下的附加功能:
繼承允許一個(gè)類(lèi)繼承另一個(gè)類(lèi)的特征
類(lèi)型轉(zhuǎn)換允許在運(yùn)行時(shí)檢查和解釋一個(gè)類(lèi)實(shí)例的類(lèi)型
析構(gòu)器允許一個(gè)類(lèi)實(shí)例釋放任何其所被分配的資源
引用計(jì)數(shù)允許對(duì)一個(gè)類(lèi)的多次引用
注意
結(jié)構(gòu)體總是通過(guò)被復(fù)制的方式在代碼中傳遞对省,不使用引用計(jì)數(shù)蝗拿。
57.結(jié)構(gòu)體類(lèi)型的成員逐一構(gòu)造器(Memberwise Initializers for Structure Types)
所有結(jié)構(gòu)體都有一個(gè)自動(dòng)生成的成員逐一構(gòu)造器,用于初始化新結(jié)構(gòu)體實(shí)例中成員的屬性蒿涎。新實(shí)例中各個(gè)屬性的初始值可以通過(guò)屬性的名稱(chēng)傳遞到成員逐一構(gòu)造器之中:
let vga = Resolution(width:640, height: 480)
與結(jié)構(gòu)體不同哀托,類(lèi)實(shí)例沒(méi)有默認(rèn)的成員逐一構(gòu)造器。
58.結(jié)構(gòu)體和枚舉是值類(lèi)型
值類(lèi)型被賦予給一個(gè)變量劳秋、常量或者被傳遞給一個(gè)函數(shù)的時(shí)候仓手,其值會(huì)被拷貝胖齐。
在之前的章節(jié)中,我們已經(jīng)大量使用了值類(lèi)型俗或。實(shí)際上,在Swift中岁忘,所有的基本類(lèi)型:整數(shù)(Integer)辛慰、浮點(diǎn)數(shù)(floating-point)、布爾值(Boolean)干像、字符串(string)帅腌、數(shù)組(array)和字典(dictionary),都是值類(lèi)型麻汰,并且在底層都是以結(jié)構(gòu)體的形式所實(shí)現(xiàn)速客。
在Swift中,所有的結(jié)構(gòu)體和枚舉類(lèi)型都是值類(lèi)型五鲫。這意味著它們的實(shí)例溺职,以及實(shí)例中所包含的任何值類(lèi)型屬性,在代碼中傳遞的時(shí)候都會(huì)被復(fù)制位喂。
請(qǐng)看下面這個(gè)示例浪耘,其使用了前一個(gè)示例中的Resolution結(jié)構(gòu)體:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
在以上示例中,聲明了一個(gè)名為hd的常量塑崖,其值為一個(gè)初始化為全高清視頻分辨率(1920像素寬七冲,1080像素高)的Resolution實(shí)例。
然后示例中又聲明了一個(gè)名為cinema的變量规婆,并將hd賦值給它。因?yàn)镽esolution是一個(gè)結(jié)構(gòu)體,所以cinema的值其實(shí)是hd的一個(gè)拷貝副本啄巧,而不是hd本身院究。盡管hd和cinema有著相同的寬(width)和高(height),但是在幕后它們是兩個(gè)完全不同的實(shí)例嗡髓。
下面通铲,為了符合數(shù)碼影院放映的需求(2048像素寬,1080像素高)器贩,cinema的width屬性需要作如下修改:
cinema.width = 2048
這里颅夺,將會(huì)顯示cinema的width屬性確已改為了2048:
print("cinema is now\(cinema.width) pixels wide")
//輸出"cinema is now 2048 pixels wide"
然而,初始的hd實(shí)例中width屬性還是1920:
print("hd is still \(hd.width) pixels wide")
//輸出"hd is still 1920 pixels wide"
在將hd賦予給cinema的時(shí)候蛹稍,實(shí)際上是將hd中所存儲(chǔ)的值進(jìn)行拷貝吧黄,然后將拷貝的數(shù)據(jù)存儲(chǔ)到新的cinema實(shí)例中。結(jié)果就是兩個(gè)完全獨(dú)立的實(shí)例碰巧包含有相同的數(shù)值唆姐。由于兩者相互獨(dú)立拗慨,因此將cinema的width修改為2048并不會(huì)影響hd中的width的值。
枚舉也遵循相同的行為準(zhǔn)則:
enum CompassPoint {
case North, South, East, West
}
var currentDirection = CompassPoint.West
let rememberedDirection = currentDirection
currentDirection = .East
if rememberedDirection == .West {
print("The remembered direction is still .West")
}
//輸出"The remembered direction is still .West"
上例中rememberedDirection被賦予了currentDirection的值,實(shí)際上它被賦予的是值的一個(gè)拷貝赵抢。賦值過(guò)程結(jié)束后再修改currentDirection的值并不影響rememberedDirection所儲(chǔ)存的原始值的拷貝剧蹂。
59.類(lèi)是引用類(lèi)型
與值類(lèi)型不同,引用類(lèi)型在被賦予到一個(gè)變量烦却、常量或者被傳遞到一個(gè)函數(shù)時(shí)宠叼,其值不會(huì)被拷貝。因此其爵,引用的是已存在的實(shí)例本身而不是其拷貝冒冬。
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
需要注意的是tenEighty和alsoTenEighty被聲明為常量而不是變量。然而你依然可以改變tenEighty.frameRate和alsoTenEighty.frameRate摩渺,因?yàn)閠enEighty和alsoTenEighty這兩個(gè)常量的值并未改變简烤。它們并不“存儲(chǔ)”這個(gè)VideoMode實(shí)例,而僅僅是對(duì)VideoMode實(shí)例的引用摇幻。所以横侦,改變的是被引用的VideoMode的frameRate屬性,而不是引用VideoMode的常量的值绰姻。
60.恒等運(yùn)算符
因?yàn)轭?lèi)是引用類(lèi)型丈咐,有可能有多個(gè)常量和變量在幕后同時(shí)引用同一個(gè)類(lèi)實(shí)例。(對(duì)于結(jié)構(gòu)體和枚舉來(lái)說(shuō)龙宏,這并不成立棵逊。因?yàn)樗鼈冏鳛橹殿?lèi)型,在被賦予到常量银酗、變量或者傳遞到函數(shù)時(shí)辆影,其值總是會(huì)被拷貝。)
如果能夠判定兩個(gè)常量或者變量是否引用同一個(gè)類(lèi)實(shí)例將會(huì)很有幫助黍特。為了達(dá)到這個(gè)目的蛙讥,Swift內(nèi)建了兩個(gè)恒等運(yùn)算符:
等價(jià)于(===)
不等價(jià)于(!==)
運(yùn)用這兩個(gè)運(yùn)算符檢測(cè)兩個(gè)常量或者變量是否引用同一個(gè)實(shí)例:
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
//輸出"tenEighty and alsoTenEighty refer to the same Resolution instance."
請(qǐng)注意,“等價(jià)于”(用三個(gè)等號(hào)表示灭衷,===)與“等于”(用兩個(gè)等號(hào)表示次慢,==)的不同:
“等價(jià)于”表示兩個(gè)類(lèi)類(lèi)型(class type)的常量或者變量引用同一個(gè)類(lèi)實(shí)例。
“等于”表示兩個(gè)實(shí)例的值“相等”或“相同”翔曲,判定時(shí)要遵照設(shè)計(jì)者定義的評(píng)判標(biāo)準(zhǔn)迫像,因此相對(duì)于“相等”來(lái)說(shuō),這是一種更加合適的叫法瞳遍。
61.指針
如果你有C闻妓,C++或者Objective-C語(yǔ)言的經(jīng)驗(yàn),那么你也許會(huì)知道這些語(yǔ)言使用指針來(lái)引用內(nèi)存中的地址掠械。一個(gè)引用某個(gè)引用類(lèi)型實(shí)例的Swift常量或者變量由缆,與C語(yǔ)言中的指針類(lèi)似注祖,但是并不直接指向某個(gè)內(nèi)存地址,也不要求你使用星號(hào)(*)來(lái)表明你在創(chuàng)建一個(gè)引用均唉。Swift中的這些引用與其它的常量或變量的定義方式相同是晨。
62.類(lèi)和結(jié)構(gòu)體的選擇
按照通用的準(zhǔn)則,當(dāng)符合一條或多條以下條件時(shí)舔箭,請(qǐng)考慮構(gòu)建結(jié)構(gòu)體:
該數(shù)據(jù)結(jié)構(gòu)的主要目的是用來(lái)封裝少量相關(guān)簡(jiǎn)單數(shù)據(jù)值罩缴。
有理由預(yù)計(jì)該數(shù)據(jù)結(jié)構(gòu)的實(shí)例在被賦值或傳遞時(shí),封裝的數(shù)據(jù)將會(huì)被拷貝而不是被引用限嫌。
該數(shù)據(jù)結(jié)構(gòu)中儲(chǔ)存的值類(lèi)型屬性靴庆,也應(yīng)該被拷貝时捌,而不是被引用怒医。
該數(shù)據(jù)結(jié)構(gòu)不需要去繼承另一個(gè)既有類(lèi)型的屬性或者行為。
舉例來(lái)說(shuō)奢讨,以下情境中適合使用結(jié)構(gòu)體:
幾何形狀的大小稚叹,封裝一個(gè)width屬性和height屬性,兩者均為Double類(lèi)型拿诸。
一定范圍內(nèi)的路徑扒袖,封裝一個(gè)start屬性和length屬性,兩者均為Int類(lèi)型亩码。
三維坐標(biāo)系內(nèi)一點(diǎn)季率,封裝x,y和z屬性描沟,三者均為Double類(lèi)型飒泻。
在所有其它案例中,定義一個(gè)類(lèi)吏廉,生成一個(gè)它的實(shí)例冤议,并通過(guò)引用來(lái)管理和傳遞邓夕。實(shí)際中,這意味著絕大部分的自定義數(shù)據(jù)構(gòu)造都應(yīng)該是類(lèi),而非結(jié)構(gòu)體发笔。
63.字符串(String)、數(shù)組(Array)冻记、和字典(Dictionary)類(lèi)型的賦值與復(fù)制行為
Swift中济丘,許多基本類(lèi)型,諸如String生巡,Array和Dictionary類(lèi)型均以結(jié)構(gòu)體的形式實(shí)現(xiàn)方库。這意味著被賦值給新的常量或變量,或者被傳入函數(shù)或方法中時(shí)障斋,它們的值會(huì)被拷貝纵潦。
Objective-C中NSString徐鹤,NSArray和NSDictionary類(lèi)型均以類(lèi)的形式實(shí)現(xiàn),而并非結(jié)構(gòu)體邀层。它們?cè)诒毁x值或者被傳入函數(shù)或方法時(shí)返敬,不會(huì)發(fā)生值拷貝,而是傳遞現(xiàn)有實(shí)例的引用寥院。
注意
以上是對(duì)字符串劲赠、數(shù)組、字典的“拷貝”行為的描述秸谢。在你的代碼中凛澎,拷貝行為看起來(lái)似乎總會(huì)發(fā)生。然而估蹄,Swift在幕后只在絕對(duì)必要時(shí)才執(zhí)行實(shí)際的拷貝塑煎。Swift管理所有的值拷貝以確保性能最優(yōu)化,所以你沒(méi)必要去回避賦值來(lái)保證性能最優(yōu)化臭蚁。
64.存儲(chǔ)屬性
簡(jiǎn)單來(lái)說(shuō)最铁,一個(gè)存儲(chǔ)屬性就是存儲(chǔ)在特定類(lèi)或結(jié)構(gòu)體實(shí)例里的一個(gè)常量或變量。存儲(chǔ)屬性可以是變量存儲(chǔ)屬性(用關(guān)鍵字var定義)垮兑,也可以是常量存儲(chǔ)屬性(用關(guān)鍵字let定義)冷尉。
65.常量結(jié)構(gòu)體的存儲(chǔ)屬性
如果創(chuàng)建了一個(gè)結(jié)構(gòu)體的實(shí)例并將其賦值給一個(gè)常量,則無(wú)法修改該實(shí)例的任何屬性系枪,即使有屬性被聲明為變量也不行:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
//該區(qū)間表示整數(shù)0雀哨,1,2私爷,3
rangeOfFourItems.firstValue = 6
//盡管firstValue是個(gè)變量屬性雾棺,這里還是會(huì)報(bào)錯(cuò)
因?yàn)閞angeOfFourItems被聲明成了常量(用let關(guān)鍵字),即使firstValue是一個(gè)變量屬性当犯,也無(wú)法再修改它了垢村。
這種行為是由于結(jié)構(gòu)體(struct)屬于值類(lèi)型。當(dāng)值類(lèi)型的實(shí)例被聲明為常量的時(shí)候嚎卫,它的所有屬性也就成了常量嘉栓。
屬于引用類(lèi)型的類(lèi)(class)則不一樣。把一個(gè)引用類(lèi)型的實(shí)例賦給一個(gè)常量后拓诸,仍然可以修改該實(shí)例的變量屬性侵佃。
66.延遲存儲(chǔ)屬性
延遲存儲(chǔ)屬性是指當(dāng)?shù)谝淮伪徽{(diào)用的時(shí)候才會(huì)計(jì)算其初始值的屬性。在屬性聲明前使用lazy來(lái)標(biāo)示一個(gè)延遲存儲(chǔ)屬性奠支。
注意
必須將延遲存儲(chǔ)屬性聲明成變量(使用var關(guān)鍵字)馋辈,因?yàn)閷傩缘某跏贾悼赡茉趯?shí)例構(gòu)造完成之后才會(huì)得到。而常量屬性在構(gòu)造過(guò)程完成之前必須要有初始值倍谜,因此無(wú)法聲明成延遲屬性迈螟。
延遲屬性很有用叉抡,當(dāng)屬性的值依賴于在實(shí)例的構(gòu)造過(guò)程結(jié)束后才會(huì)知道影響值的外部因素時(shí),或者當(dāng)獲得屬性的初始值需要復(fù)雜或大量計(jì)算時(shí)答毫,可以只在需要的時(shí)候計(jì)算它褥民。
下面的例子使用了延遲存儲(chǔ)屬性來(lái)避免復(fù)雜類(lèi)中不必要的初始化。例子中定義了DataImporter和DataManager兩個(gè)類(lèi)洗搂,下面是部分代碼:
class DataImporter {
/*
DataImporter是一個(gè)負(fù)責(zé)將外部文件中的數(shù)據(jù)導(dǎo)入的類(lèi)消返。
這個(gè)類(lèi)的初始化會(huì)消耗不少時(shí)間。
*/
var fileName = "data.txt"
//這里會(huì)提供數(shù)據(jù)導(dǎo)入功能
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
//這里會(huì)提供數(shù)據(jù)管理功能
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter實(shí)例的importer屬性還沒(méi)有被創(chuàng)建
DataManager類(lèi)包含一個(gè)名為data的存儲(chǔ)屬性耘拇,初始值是一個(gè)空的字符串(String)數(shù)組撵颊。這里沒(méi)有給出全部代碼,只需知道DataManager類(lèi)的目的是管理和提供對(duì)這個(gè)字符串?dāng)?shù)組的訪問(wèn)即可惫叛。
DataManager的一個(gè)功能是從文件導(dǎo)入數(shù)據(jù)倡勇。該功能由DataImporter類(lèi)提供,DataImporter完成初始化需要消耗不少時(shí)間:因?yàn)樗膶?shí)例在初始化時(shí)可能要打開(kāi)文件挣棕,還要讀取文件內(nèi)容到內(nèi)存译隘。
DataManager管理數(shù)據(jù)時(shí)也可能不從文件中導(dǎo)入數(shù)據(jù)亲桥。所以當(dāng)DataManager的實(shí)例被創(chuàng)建時(shí)洛心,沒(méi)必要?jiǎng)?chuàng)建一個(gè)DataImporter的實(shí)例,更明智的做法是第一次用到DataImporter的時(shí)候才去創(chuàng)建它题篷。
由于使用了lazy词身,importer屬性只有在第一次被訪問(wèn)的時(shí)候才被創(chuàng)建。比如訪問(wèn)它的屬性fileName時(shí):
print(manager.importer.fileName)
// DataImporter實(shí)例的importer屬性現(xiàn)在被創(chuàng)建了
//輸出"data.txt”
注意
如果一個(gè)被標(biāo)記為lazy的屬性在沒(méi)有初始化時(shí)就同時(shí)被多個(gè)線程訪問(wèn)番枚,則無(wú)法保證該屬性只會(huì)被初始化一次法严。
67.存儲(chǔ)屬性和實(shí)例變量
如果您有過(guò)Objective-C經(jīng)驗(yàn),應(yīng)該知道Objective-C為類(lèi)實(shí)例存儲(chǔ)值和引用提供兩種方法葫笼。除了屬性之外深啤,還可以使用實(shí)例變量作為屬性值的后端存儲(chǔ)。
Swift編程語(yǔ)言中把這些理論統(tǒng)一用屬性來(lái)實(shí)現(xiàn)路星。Swift中的屬性沒(méi)有對(duì)應(yīng)的實(shí)例變量溯街,屬性的后端存儲(chǔ)也無(wú)法直接訪問(wèn)。這就避免了不同場(chǎng)景下訪問(wèn)方式的困擾洋丐,同時(shí)也將屬性的定義簡(jiǎn)化成一個(gè)語(yǔ)句呈昔。屬性的全部信息——包括命名肪笋、類(lèi)型和內(nèi)存管理特征——都在唯一一個(gè)地方(類(lèi)型定義中)定義墙懂。
68.計(jì)算屬性
除存儲(chǔ)屬性外,類(lèi)媒殉、結(jié)構(gòu)體和枚舉可以定義計(jì)算屬性迁客。計(jì)算屬性不直接存儲(chǔ)值郭宝,而是提供一個(gè)getter和一個(gè)可選的setter辞槐,來(lái)間接獲取和設(shè)置其他屬性或變量的值。
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
//輸出"square.origin is now at (10.0, 10.0)”
69.便捷setter聲明
如果計(jì)算屬性的setter沒(méi)有定義表示新值的參數(shù)名粘室,則可以使用默認(rèn)名稱(chēng)newValue催蝗。下面是使用了便捷setter聲明的Rect結(jié)構(gòu)體代碼:
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
70.只讀計(jì)算屬性
只有g(shù)etter沒(méi)有setter的計(jì)算屬性就是只讀計(jì)算屬性。只讀計(jì)算屬性總是返回一個(gè)值育特,可以通過(guò)點(diǎn)運(yùn)算符訪問(wèn)丙号,但不能設(shè)置新的值。
注意
必須使用var關(guān)鍵字定義計(jì)算屬性缰冤,包括只讀計(jì)算屬性犬缨,因?yàn)樗鼈兊闹挡皇枪潭ǖ?/b>。let關(guān)鍵字只用來(lái)聲明常量屬性棉浸,表示初始化后再也無(wú)法修改的值怀薛。
只讀計(jì)算屬性的聲明可以去掉get關(guān)鍵字和花括號(hào):
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
//輸出"the volume of fourByFiveByTwo is 40.0"
這個(gè)例子定義了一個(gè)名為Cuboid的結(jié)構(gòu)體,表示三維空間的立方體迷郑,包含width枝恋、height和depth屬性。結(jié)構(gòu)體還有一個(gè)名為volume的只讀計(jì)算屬性用來(lái)返回立方體的體積嗡害。為volume提供setter毫無(wú)意義焚碌,因?yàn)闊o(wú)法確定如何修改width、height和depth三者的值來(lái)匹配新的volume霸妹。然而十电,Cuboid提供一個(gè)只讀計(jì)算屬性來(lái)讓外部用戶直接獲取體積是很有用的。
71.屬性觀察器
屬性觀察器監(jiān)控和響應(yīng)屬性值的變化叹螟,每次屬性被設(shè)置值的時(shí)候都會(huì)調(diào)用屬性觀察器鹃骂,即使新值和當(dāng)前值相同的時(shí)候也不例外。
可以為除了延遲存儲(chǔ)屬性之外的其他存儲(chǔ)屬性添加屬性觀察器罢绽,也可以通過(guò)重寫(xiě)屬性的方式為繼承的屬性(包括存儲(chǔ)屬性和計(jì)算屬性)添加屬性觀察器畏线。你不必為非重寫(xiě)的計(jì)算屬性添加屬性觀察器,因?yàn)榭梢酝ㄟ^(guò)它的setter直接監(jiān)控和響應(yīng)值的變化良价。屬性重寫(xiě)請(qǐng)參考重寫(xiě)寝殴。
可以為屬性添加如下的一個(gè)或全部觀察器:
willSet在新的值被設(shè)置之前調(diào)用
didSet在新的值被設(shè)置之后立即調(diào)用
willSet觀察器會(huì)將新的屬性值作為常量參數(shù)傳入,在willSet的實(shí)現(xiàn)代碼中可以為這個(gè)參數(shù)指定一個(gè)名稱(chēng)棚壁,如果不指定則參數(shù)仍然可用杯矩,這時(shí)使用默認(rèn)名稱(chēng)newValue表示。
同樣袖外,didSet觀察器會(huì)將舊的屬性值作為參數(shù)傳入史隆,可以為該參數(shù)命名或者使用默認(rèn)參數(shù)名oldValue。如果在didSet方法中再次對(duì)該屬性賦值曼验,那么新值會(huì)覆蓋舊的值泌射。
例子:
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue{
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
注意
父類(lèi)的屬性在子類(lèi)的構(gòu)造器中被賦值時(shí)粘姜,它在父類(lèi)中的willSet和didSet觀察器會(huì)被調(diào)用,隨后才會(huì)調(diào)用子類(lèi)的觀察器熔酷。在父類(lèi)書(shū)初始化方法調(diào)用之前孤紧,子類(lèi)給屬性賦值時(shí),觀察器不會(huì)被調(diào)用拒秘。
注意
如果將屬性通過(guò)in-out方式傳入函數(shù)号显,willSet和didSet也會(huì)調(diào)用。這是因?yàn)閕n-out參數(shù)采用了拷入拷出模式:即在函數(shù)內(nèi)部使用的是參數(shù)的copy躺酒,函數(shù)結(jié)束后押蚤,又對(duì)參數(shù)重新賦值
72.全局變量和局部變量
計(jì)算屬性和屬性觀察器所描述的功能也可以用于全局變量和局部變量。全局變量是在函數(shù)羹应、方法揽碘、閉包或任何類(lèi)型之外定義的變量。局部變量是在函數(shù)园匹、方法或閉包內(nèi)部定義的變量雳刺。
注意
全局的常量或變量都是延遲計(jì)算的,跟延遲存儲(chǔ)屬性相似裸违,不同的地方在于掖桦,全局的常量或變量不需要標(biāo)記lazy修飾符。
局部范圍的常量或變量從不延遲計(jì)算累颂。
73.類(lèi)型屬性
實(shí)例屬性屬于一個(gè)特定類(lèi)型的實(shí)例滞详,每創(chuàng)建一個(gè)實(shí)例凛俱,實(shí)例都擁有屬于自己的一套屬性值紊馏,實(shí)例之間的屬性相互獨(dú)立。
也可以為類(lèi)型本身定義屬性蒲犬,無(wú)論創(chuàng)建了多少個(gè)該類(lèi)型的實(shí)例朱监,這些屬性都只有唯一一份。這種屬性就是類(lèi)型屬性原叮。
類(lèi)型屬性用于定義某個(gè)類(lèi)型所有實(shí)例共享的數(shù)據(jù)赫编,比如所有實(shí)例都能用的一個(gè)常量(就像C語(yǔ)言中的靜態(tài)常量),或者所有實(shí)例都能訪問(wèn)的一個(gè)變量(就像C語(yǔ)言中的靜態(tài)變量)奋隶。
存儲(chǔ)型類(lèi)型屬性可以是變量或常量擂送,計(jì)算型類(lèi)型屬性跟實(shí)例的計(jì)算型屬性一樣只能定義成變量屬性。
注意
跟實(shí)例的存儲(chǔ)型屬性不同唯欣,必須給存儲(chǔ)型類(lèi)型屬性指定默認(rèn)值嘹吨,因?yàn)轭?lèi)型本身沒(méi)有構(gòu)造器,也就無(wú)法在初始化過(guò)程中使用構(gòu)造器給類(lèi)型屬性賦值境氢。
存儲(chǔ)型類(lèi)型屬性是延遲初始化的蟀拷,它們只有在第一次被訪問(wèn)的時(shí)候才會(huì)被初始化碰纬。即使它們被多個(gè)線程同時(shí)訪問(wèn),系統(tǒng)也保證只會(huì)對(duì)其進(jìn)行一次初始化问芬,并且不需要對(duì)其使用lazy修飾符。
74.類(lèi)型屬性語(yǔ)法
在C或Objective-C中,與某個(gè)類(lèi)型關(guān)聯(lián)的靜態(tài)常量和靜態(tài)變量惹悄,是作為全局(global)靜態(tài)變量定義的悉患。但是在Swift中,類(lèi)型屬性是作為類(lèi)型定義的一部分寫(xiě)在類(lèi)型最外層的花括號(hào)內(nèi)挡鞍,因此它的作用范圍也就在類(lèi)型支持的范圍內(nèi)酌泰。
使用關(guān)鍵字static來(lái)定義類(lèi)型屬性。在為類(lèi)定義計(jì)算型類(lèi)型屬性時(shí)匕累,可以改用關(guān)鍵字class來(lái)支持子類(lèi)對(duì)父類(lèi)的實(shí)現(xiàn)進(jìn)行重寫(xiě)陵刹。下面的例子演示了存儲(chǔ)型和計(jì)算型類(lèi)型屬性的語(yǔ)法:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
75.獲取和設(shè)置類(lèi)型屬性的值
跟實(shí)例屬性一樣,類(lèi)型屬性也是通過(guò)點(diǎn)運(yùn)算符來(lái)訪問(wèn)欢嘿。但是衰琐,類(lèi)型屬性是通過(guò)類(lèi)型本身來(lái)訪問(wèn),而不是通過(guò)實(shí)例炼蹦。比如:
print(SomeStructure.storedTypeProperty)
//輸出"Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
//輸出"Another value.”
print(SomeEnumeration.computedTypeProperty)
//輸出"6"
print(SomeClass.computedTypeProperty)
//輸出"27"
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
//將當(dāng)前音量限制在閥值之內(nèi)
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
//存儲(chǔ)當(dāng)前音量作為新的最大輸入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
76.方法(Methods)
方法是與某些特定類(lèi)型相關(guān)聯(lián)的函數(shù)羡宙。類(lèi)、結(jié)構(gòu)體掐隐、枚舉都可以定義實(shí)例方法狗热;實(shí)例方法為給定類(lèi)型的實(shí)例封裝了具體的任務(wù)與功能。類(lèi)虑省、結(jié)構(gòu)體匿刮、枚舉也可以定義類(lèi)型方法;類(lèi)型方法與類(lèi)型本身相關(guān)聯(lián)探颈。類(lèi)型方法與Objective-C中的類(lèi)方法(class methods)相似熟丸。
結(jié)構(gòu)體和枚舉能夠定義方法是Swift與C/Objective-C的主要區(qū)別之一。在Objective-C中伪节,類(lèi)是唯一能定義方法的類(lèi)型光羞。但在Swift中,你不僅能選擇是否要定義一個(gè)類(lèi)/結(jié)構(gòu)體/枚舉怀大,還能靈活地在你創(chuàng)建的類(lèi)型(類(lèi)/結(jié)構(gòu)體/枚舉)上定義方法纱兑。
77.修改方法的外部參數(shù)名稱(chēng)(Modifying External Parameter Name Behavior for Methods)
有時(shí)為方法的第一個(gè)參數(shù)提供一個(gè)外部參數(shù)名稱(chēng)是非常有用的,盡管這不是默認(rèn)的行為化借。你自己可以為第一個(gè)參數(shù)添加一個(gè)顯式的外部名稱(chēng)潜慎。
相反,如果你不想為方法的第二個(gè)及后續(xù)的參數(shù)提供一個(gè)外部名稱(chēng),可以通過(guò)使用下劃線(_)作為該參數(shù)的顯式外部名稱(chēng)勘纯,這樣做將覆蓋默認(rèn)行為局服。
78.實(shí)例方法(Instance Methods)
實(shí)例方法是屬于某個(gè)特定類(lèi)、結(jié)構(gòu)體或者枚舉類(lèi)型實(shí)例的方法驳遵。實(shí)例方法提供訪問(wèn)和修改實(shí)例屬性的方法或提供與實(shí)例目的相關(guān)的功能淫奔,并以此來(lái)支撐實(shí)例的功能。實(shí)例方法的語(yǔ)法與函數(shù)完全一致堤结,詳情參見(jiàn)函數(shù)
class Counter {
var count = 0
func increment() {
++count
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
Counter類(lèi)定義了三個(gè)實(shí)例方法:
increment讓計(jì)數(shù)器按一遞增唆迁;
incrementBy(amount: Int)讓計(jì)數(shù)器按一個(gè)指定的整數(shù)值遞增;
reset將計(jì)數(shù)器重置為0竞穷。
Counter這個(gè)類(lèi)還聲明了一個(gè)可變屬性count唐责,用它來(lái)保持對(duì)當(dāng)前計(jì)數(shù)器值的追蹤。
和調(diào)用屬性一樣瘾带,用點(diǎn)語(yǔ)法(dot syntax)調(diào)用實(shí)例方法:
let counter = Counter()
//初始計(jì)數(shù)值是0
counter.increment()
//計(jì)數(shù)值現(xiàn)在是1
counter.incrementBy(5)
//計(jì)數(shù)值現(xiàn)在是6
counter.reset()
//計(jì)數(shù)值現(xiàn)在是0
79.self屬性(The self Property)
類(lèi)型的每一個(gè)實(shí)例都有一個(gè)隱含屬性叫做self鼠哥,self完全等同于該實(shí)例本身。你可以在一個(gè)實(shí)例的實(shí)例方法中使用這個(gè)隱含的self屬性來(lái)引用當(dāng)前實(shí)例看政。
上面例子中的increment方法還可以這樣寫(xiě):
func increment() {
self.count++
}
實(shí)際上朴恳,你不必在你的代碼里面經(jīng)常寫(xiě)self。不論何時(shí)允蚣,只要在一個(gè)方法中使用一個(gè)已知的屬性或者方法名稱(chēng)于颖,如果你沒(méi)有明確地寫(xiě)self,Swift假定你是指當(dāng)前實(shí)例的屬性或者方法嚷兔。這種假定在上面的Counter中已經(jīng)示范了:Counter中的三個(gè)實(shí)例方法中都使用的是count(而不是self.count)森渐。
使用這條規(guī)則的主要場(chǎng)景是實(shí)例方法的某個(gè)參數(shù)名稱(chēng)與實(shí)例的某個(gè)屬性名稱(chēng)相同的時(shí)候。在這種情況下冒晰,參數(shù)名稱(chēng)享有優(yōu)先權(quán)同衣,并且在引用屬性時(shí)必須使用一種更嚴(yán)格的方式。這時(shí)你可以使用self屬性來(lái)區(qū)分參數(shù)名稱(chēng)和屬性名稱(chēng)翩剪。
下面的例子中乳怎,self消除方法參數(shù)x和實(shí)例屬性x之間的歧義:
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOfX(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOfX(1.0) {
print("This point is to the right of the line where x == 1.0")
}
//打印輸出: This point is to the right of the line where x == 1.0
如果不使用self前綴,Swift就認(rèn)為兩次使用的x都指的是名稱(chēng)為x的函數(shù)參數(shù)前弯。
80.在實(shí)例方法中修改值類(lèi)型(Modifying Value Types from Within Instance Methods)
結(jié)構(gòu)體和枚舉是值類(lèi)型。默認(rèn)情況下秫逝,值類(lèi)型的屬性不能在它的實(shí)例方法中被修改恕出。
但是,如果你確實(shí)需要在某個(gè)特定的方法中修改結(jié)構(gòu)體或者枚舉的屬性违帆,你可以為這個(gè)方法選擇可變(mutating)行為浙巫,然后就可以從其方法內(nèi)部改變它的屬性;并且這個(gè)方法做的任何改變都會(huì)在方法執(zhí)行結(jié)束時(shí)寫(xiě)回到原始結(jié)構(gòu)中。方法還可以給它隱含的self屬性賦予一個(gè)全新的實(shí)例的畴,這個(gè)新實(shí)例在方法結(jié)束時(shí)會(huì)替換現(xiàn)存實(shí)例渊抄。
要使用可變方法,將關(guān)鍵字mutating放到方法的func關(guān)鍵字之前就可以了:
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveByX(2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
//打印輸出: "The point is now at (3.0, 4.0)"
上面的Point結(jié)構(gòu)體定義了一個(gè)可變方法moveByX(_:y:)來(lái)移動(dòng)Point實(shí)例到給定的位置丧裁。該方法被調(diào)用時(shí)修改了這個(gè)點(diǎn)护桦,而不是返回一個(gè)新的點(diǎn)。方法定義時(shí)加上了mutating關(guān)鍵字煎娇,從而允許修改屬性二庵。
注意,不能在結(jié)構(gòu)體類(lèi)型的常量(a constant of structure type)上調(diào)用可變方法缓呛,因?yàn)槠鋵傩圆荒鼙桓淖兇呦恚词箤傩允亲兞繉傩裕斍閰⒁?jiàn)常量結(jié)構(gòu)體的存儲(chǔ)屬性:
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveByX(2.0, y: 3.0)
//這里將會(huì)報(bào)告一個(gè)錯(cuò)誤
81.在可變方法中給self賦值(Assigning to self Within a Mutating Method)
可變方法能夠賦給隱含屬性self一個(gè)全新的實(shí)例哟绊。上面Point的例子可以用下面的方式改寫(xiě):
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
新版的可變方法moveByX(_:y:)創(chuàng)建了一個(gè)新的結(jié)構(gòu)體實(shí)例因妙,它的x和y的值都被設(shè)定為目標(biāo)值。調(diào)用這個(gè)版本的方法和調(diào)用上個(gè)版本的最終結(jié)果是一樣的票髓。
枚舉的可變方法可以把self設(shè)置為同一枚舉類(lèi)型中不同的成員:
enum TriStateSwitch {
case Off, Low, High
mutating func next() {
switch self {
case Off:
self = Low
case Low:
self = High
case High:
self = Off
}
}
}
var ovenLight = TriStateSwitch.Low
ovenLight.next()
// ovenLight現(xiàn)在等于.High
ovenLight.next()
// ovenLight現(xiàn)在等于.Off
上面的例子中定義了一個(gè)三態(tài)開(kāi)關(guān)的枚舉兰迫。每次調(diào)用next()方法時(shí)券坞,開(kāi)關(guān)在不同的電源狀態(tài)(Off万牺,Low胁澳,High)之間循環(huán)切換走诞。
82.類(lèi)型方法(Type Methods)
在方法的func關(guān)鍵字之前加上關(guān)鍵字static收奔,來(lái)指定類(lèi)型方法痰驱。類(lèi)還可以用關(guān)鍵字class來(lái)允許子類(lèi)重寫(xiě)父類(lèi)的方法實(shí)現(xiàn)肩民。
注意
在Objective-C中蔽豺,你只能為Objective-C的類(lèi)類(lèi)型(classes)定義類(lèi)型方法(type-level methods)跷车。在Swift中棘利,你可以為所有的類(lèi)、結(jié)構(gòu)體和枚舉定義類(lèi)型方法朽缴。每一個(gè)類(lèi)型方法都被它所支持的類(lèi)型顯式包含善玫。
類(lèi)型方法和實(shí)例方法一樣用點(diǎn)語(yǔ)法調(diào)用。但是密强,你是在類(lèi)型上調(diào)用這個(gè)方法茅郎,而不是在實(shí)例上調(diào)用。下面是如何在SomeClass類(lèi)上調(diào)用類(lèi)型方法的例子:
class SomeClass {
class func someTypeMethod() {
// type method implementation goes here
}
}
SomeClass.someTypeMethod()
在類(lèi)型方法的方法體(body)中或渤,self指向這個(gè)類(lèi)型本身系冗,而不是類(lèi)型的某個(gè)實(shí)例。這意味著你可以用self來(lái)消除類(lèi)型屬性和類(lèi)型方法參數(shù)之間的歧義(類(lèi)似于我們?cè)谇懊嫣幚韺?shí)例屬性和實(shí)例方法參數(shù)時(shí)做的那樣)薪鹦。
一般來(lái)說(shuō)掌敬,在類(lèi)型方法的方法體中惯豆,任何未限定的方法和屬性名稱(chēng),可以被本類(lèi)中其他的類(lèi)型方法和類(lèi)型屬性引用奔害。一個(gè)類(lèi)型方法可以直接通過(guò)類(lèi)型方法的名稱(chēng)調(diào)用本類(lèi)中的其它類(lèi)型方法楷兽,而無(wú)需在方法名稱(chēng)前面加上類(lèi)型名稱(chēng)。類(lèi)似地华临,在結(jié)構(gòu)體和枚舉中芯杀,也能夠直接通過(guò)類(lèi)型屬性的名稱(chēng)訪問(wèn)本類(lèi)中的類(lèi)型屬性,而不需要前面加上類(lèi)型名稱(chēng)银舱。
下面的例子定義了一個(gè)名為L(zhǎng)evelTracker結(jié)構(gòu)體瘪匿。它監(jiān)測(cè)玩家的游戲發(fā)展情況(游戲的不同層次或階段)。這是一個(gè)單人游戲寻馏,但也可以存儲(chǔ)多個(gè)玩家在同一設(shè)備上的游戲信息棋弥。
游戲初始時(shí),所有的游戲等級(jí)(除了等級(jí)1)都被鎖定诚欠。每次有玩家完成一個(gè)等級(jí)顽染,這個(gè)等級(jí)就對(duì)這個(gè)設(shè)備上的所有玩家解鎖。LevelTracker結(jié)構(gòu)體用類(lèi)型屬性和方法監(jiān)測(cè)游戲的哪個(gè)等級(jí)已經(jīng)被解鎖轰绵。它還監(jiān)測(cè)每個(gè)玩家的當(dāng)前等級(jí)粉寞。
struct LevelTracker {
static var highestUnlockedLevel = 1
static func unlockLevel(level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}
static func levelIsUnlocked(level: Int) -> Bool {
return level <= highestUnlockedLevel
}
var currentLevel = 1
mutating func advanceToLevel(level: Int) -> Bool {
if LevelTracker.levelIsUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
LevelTracker監(jiān)測(cè)玩家已解鎖的最高等級(jí)。這個(gè)值被存儲(chǔ)在類(lèi)型屬性highestUnlockedLevel中左腔。
LevelTracker還定義了兩個(gè)類(lèi)型方法與highestUnlockedLevel配合工作唧垦。第一個(gè)類(lèi)型方法是unlockLevel,一旦新等級(jí)被解鎖液样,它會(huì)更新highestUnlockedLevel的值振亮。第二個(gè)類(lèi)型方法是levelIsUnlocked,如果某個(gè)給定的等級(jí)已經(jīng)被解鎖鞭莽,它將返回true坊秸。(注意,盡管我們沒(méi)有使用類(lèi)似LevelTracker.highestUnlockedLevel的寫(xiě)法澎怒,這個(gè)類(lèi)型方法還是能夠訪問(wèn)類(lèi)型屬性highestUnlockedLevel)
除了類(lèi)型屬性和類(lèi)型方法褒搔,LevelTracker還監(jiān)測(cè)每個(gè)玩家的進(jìn)度。它用實(shí)例屬性currentLevel來(lái)監(jiān)測(cè)每個(gè)玩家當(dāng)前的等級(jí)喷面。
為了便于管理currentLevel屬性星瘾,LevelTracker定義了實(shí)例方法advanceToLevel。這個(gè)方法會(huì)在更新currentLevel之前檢查所請(qǐng)求的新等級(jí)是否已經(jīng)解鎖乖酬。advanceToLevel方法返回布爾值以指示是否能夠設(shè)置currentLevel死相。
下面,Player類(lèi)使用LevelTracker來(lái)監(jiān)測(cè)和更新每個(gè)玩家的發(fā)展進(jìn)度:
class Player {
var tracker = LevelTracker()
let playerName: String
func completedLevel(level: Int) {
LevelTracker.unlockLevel(level + 1)
tracker.advanceToLevel(level + 1)
}
init(name: String) {
playerName = name
}
}
Player類(lèi)創(chuàng)建一個(gè)新的LevelTracker實(shí)例來(lái)監(jiān)測(cè)這個(gè)用戶的進(jìn)度咬像。它提供了completedLevel方法,一旦玩家完成某個(gè)指定等級(jí)就調(diào)用它。這個(gè)方法為所有玩家解鎖下一等級(jí)县昂,并且將當(dāng)前玩家的進(jìn)度更新為下一等級(jí)肮柜。(我們忽略了advanceToLevel返回的布爾值,因?yàn)橹罢{(diào)用LevelTracker.unlockLevel時(shí)就知道了這個(gè)等級(jí)已經(jīng)被解鎖了)倒彰。
你還可以為一個(gè)新的玩家創(chuàng)建一個(gè)Player的實(shí)例审洞,然后看這個(gè)玩家完成等級(jí)一時(shí)發(fā)生了什么:
var player = Player(name: "Argyrios")
player.completedLevel(1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
//打印輸出:highest unlocked level is now 2
如果你創(chuàng)建了第二個(gè)玩家,并嘗試讓他開(kāi)始一個(gè)沒(méi)有被任何玩家解鎖的等級(jí)待讳,那么試圖設(shè)置玩家當(dāng)前等級(jí)將會(huì)失斆⒗健:
player = Player(name: "Beto")
if player.tracker.advanceToLevel(6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}
//打印輸出:level 6 has not yet been unlocked
83.下標(biāo)語(yǔ)法
下標(biāo)允許你通過(guò)在實(shí)例名稱(chēng)后面的方括號(hào)中傳入一個(gè)或者多個(gè)索引值來(lái)對(duì)實(shí)例進(jìn)行存取。語(yǔ)法類(lèi)似于實(shí)例方法語(yǔ)法和計(jì)算型屬性語(yǔ)法的混合创淡。與定義實(shí)例方法類(lèi)似痴晦,定義下標(biāo)使用subscript關(guān)鍵字,指定一個(gè)或多個(gè)輸入?yún)?shù)和返回類(lèi)型琳彩;與實(shí)例方法不同的是誊酌,下標(biāo)可以設(shè)定為讀寫(xiě)或只讀。這種行為由getter和setter實(shí)現(xiàn)露乏,有點(diǎn)類(lèi)似計(jì)算型屬性:
subscript(index: Int) -> Int {
get {
//返回一個(gè)適當(dāng)?shù)腎nt類(lèi)型的值
}
set(newValue) {
//執(zhí)行適當(dāng)?shù)馁x值操作
}
}
newValue的類(lèi)型和下標(biāo)的返回類(lèi)型相同碧浊。如同計(jì)算型屬性,可以不指定setter的參數(shù)(newValue)瘟仿。如果不指定參數(shù)箱锐,setter會(huì)提供一個(gè)名為newValue的默認(rèn)參數(shù)。
如同只讀計(jì)算型屬性劳较,可以省略只讀下標(biāo)的get關(guān)鍵字:
subscript(index: Int) -> Int {
//返回一個(gè)適當(dāng)?shù)腎nt類(lèi)型的值
}
下面代碼演示了只讀下標(biāo)的實(shí)現(xiàn)驹止,這里定義了一個(gè)TimesTable結(jié)構(gòu)體,用來(lái)表示傳入整數(shù)的乘法表:
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
//輸出"six times three is 18"
在上例中兴想,創(chuàng)建了一個(gè)TimesTable實(shí)例幢哨,用來(lái)表示整數(shù)3的乘法表。數(shù)值3被傳遞給結(jié)構(gòu)體的構(gòu)造函數(shù)嫂便,作為實(shí)例成員multiplier的值伪煤。
你可以通過(guò)下標(biāo)訪問(wèn)threeTimesTable實(shí)例,例如上面演示的threeTimesTable[6]锥债。這條語(yǔ)句查詢了3的乘法表中的第六個(gè)元素错敢,返回3的6倍即18。
注意
TimesTable例子基于一個(gè)固定的數(shù)學(xué)公式厂画,對(duì)threeTimesTable[someIndex]進(jìn)行賦值操作并不合適凸丸,因此下標(biāo)定義為只讀的。
84.下標(biāo)用法
注意
Swift的Dictionary類(lèi)型的下標(biāo)接受并返回可選類(lèi)型的值袱院。上例中的numberOfLegs字典通過(guò)下標(biāo)返回的是一個(gè)Int?或者說(shuō)“可選的int”屎慢。Dictionary類(lèi)型之所以如此實(shí)現(xiàn)下標(biāo)瞭稼,是因?yàn)椴皇敲總€(gè)鍵都有個(gè)對(duì)應(yīng)的值,同時(shí)這也提供了一種通過(guò)鍵刪除對(duì)應(yīng)值的方式腻惠,只需將鍵對(duì)應(yīng)的值賦值為nil即可环肘。
85.下標(biāo)選項(xiàng)
下標(biāo)可以接受任意數(shù)量的入?yún)ⅲ⑶疫@些入?yún)⒖梢允侨我忸?lèi)型集灌。下標(biāo)的返回值也可以是任意類(lèi)型悔雹。下標(biāo)可以使用變量參數(shù)和可變參數(shù),但不能使用輸入輸出參數(shù)欣喧,也不能給參數(shù)設(shè)置默認(rèn)值腌零。
一個(gè)類(lèi)或結(jié)構(gòu)體可以根據(jù)自身需要提供多個(gè)下標(biāo)實(shí)現(xiàn),使用下標(biāo)時(shí)將通過(guò)入?yún)⒌臄?shù)量和類(lèi)型進(jìn)行區(qū)分唆阿,自動(dòng)匹配合適的下標(biāo)益涧,這就是下標(biāo)的重載。
雖然接受單一入?yún)⒌南聵?biāo)是最常見(jiàn)的酷鸦,但也可以根據(jù)情況定義接受多個(gè)入?yún)⒌南聵?biāo)饰躲。例如下例定義了一個(gè)Matrix結(jié)構(gòu)體,用于表示一個(gè)Double類(lèi)型的二維矩陣臼隔。Matrix結(jié)構(gòu)體的下標(biāo)接受兩個(gè)整型參數(shù):
struct Matrix {
let rows: Int, columns: Int
var grid: [Double]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(count: rows * columns, repeatedValue: 0.0)
}
func indexIsValidForRow(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Double {
get {
assert(indexIsValidForRow(row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValidForRow(row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
}
}
Matrix提供了一個(gè)接受兩個(gè)入?yún)⒌臉?gòu)造方法嘹裂,入?yún)⒎謩e是rows和columns,創(chuàng)建了一個(gè)足夠容納rows * columns個(gè)Double類(lèi)型的值的數(shù)組摔握。通過(guò)傳入數(shù)組長(zhǎng)度和初始值0.0到數(shù)組的構(gòu)造器寄狼,將矩陣中每個(gè)位置的值初始化為0.0。關(guān)于數(shù)組的這種構(gòu)造方法請(qǐng)參考創(chuàng)建一個(gè)空數(shù)組氨淌。
你可以通過(guò)傳入合適的row和column的數(shù)量來(lái)構(gòu)造一個(gè)新的Matrix實(shí)例:
var matrix = Matrix(rows: 2, columns: 2)
上例中創(chuàng)建了一個(gè)Matrix實(shí)例來(lái)表示兩行兩列的矩陣泊愧。該Matrix實(shí)例的grid數(shù)組按照從左上到右下的閱讀順序?qū)⒕仃嚤馄交鎯?chǔ):
將row和column的值傳入下標(biāo)來(lái)為矩陣設(shè)值,下標(biāo)的入?yún)⑹褂枚禾?hào)分隔:
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2
上面兩條語(yǔ)句分別調(diào)用下標(biāo)的setter將矩陣右上角位置(即row為0盛正、column為1的位置)的值設(shè)置為1.5删咱,將矩陣左下角位置(即row為1、column為0的位置)的值設(shè)置為3.2:
Matrix下標(biāo)的getter和setter中都含有斷言豪筝,用來(lái)檢查下標(biāo)入?yún)ow和column的值是否有效痰滋。為了方便進(jìn)行斷言,Matrix包含了一個(gè)名為indexIsValidForRow(_:column:)的便利方法续崖,用來(lái)檢查入?yún)ow和column的值是否在矩陣范圍內(nèi):
func indexIsValidForRow(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
斷言在下標(biāo)越界時(shí)觸發(fā):
let someValue = matrix[2, 2]
//斷言將會(huì)觸發(fā)敲街,因?yàn)閇2, 2]已經(jīng)超過(guò)了matrix的范圍
86.重寫(xiě)屬性
你可以重寫(xiě)繼承來(lái)的實(shí)例屬性或類(lèi)型屬性,提供自己定制的getter和setter严望,或添加屬性觀察器使重寫(xiě)的屬性可以觀察屬性值什么時(shí)候發(fā)生改變多艇。
重寫(xiě)屬性的Getters和Setters
你可以提供定制的getter(或setter)來(lái)重寫(xiě)任意繼承來(lái)的屬性,無(wú)論繼承來(lái)的屬性是存儲(chǔ)型的還是計(jì)算型的屬性像吻。子類(lèi)并不知道繼承來(lái)的屬性是存儲(chǔ)型的還是計(jì)算型的峻黍,它只知道繼承來(lái)的屬性會(huì)有一個(gè)名字和類(lèi)型复隆。你在重寫(xiě)一個(gè)屬性時(shí),必需將它的名字和類(lèi)型都寫(xiě)出來(lái)奸披。這樣才能使編譯器去檢查你重寫(xiě)的屬性是與超類(lèi)中同名同類(lèi)型的屬性相匹配的昏名。
你可以將一個(gè)繼承來(lái)的只讀屬性重寫(xiě)為一個(gè)讀寫(xiě)屬性涮雷,只需要在重寫(xiě)版本的屬性里提供getter和setter即可阵面。但是,你不可以將一個(gè)繼承來(lái)的讀寫(xiě)屬性重寫(xiě)為一個(gè)只讀屬性
注意
如果你在重寫(xiě)屬性中提供了setter洪鸭,那么你也一定要提供getter样刷。如果你不想在重寫(xiě)版本中的getter里修改繼承來(lái)的屬性值,你可以直接通過(guò)super.someProperty來(lái)返回繼承來(lái)的值览爵,其中someProperty是你要重寫(xiě)的屬性的名字置鼻。
重寫(xiě)屬性觀察器(Property Observer)
你可以通過(guò)重寫(xiě)屬性為一個(gè)繼承來(lái)的屬性添加屬性觀察器。這樣一來(lái)蜓竹,當(dāng)繼承來(lái)的屬性值發(fā)生改變時(shí)箕母,你就會(huì)被通知到,無(wú)論那個(gè)屬性原本是如何實(shí)現(xiàn)的俱济。關(guān)于屬性觀察器的更多內(nèi)容嘶是,請(qǐng)看屬性觀察器。
注意
你不可以為繼承來(lái)的常量存儲(chǔ)型屬性或繼承來(lái)的只讀計(jì)算型屬性添加屬性觀察器蛛碌。這些屬性的值是不可以被設(shè)置的聂喇,所以,為它們提供willSet或didSet實(shí)現(xiàn)是不恰當(dāng)蔚携。
此外還要注意希太,你不可以同時(shí)提供重寫(xiě)的setter和重寫(xiě)的屬性觀察器。如果你想觀察屬性值的變化酝蜒,并且你已經(jīng)為那個(gè)屬性提供了定制的setter誊辉,那么你在setter中就可以觀察到任何值變化了。
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}
87.防止重寫(xiě)
你可以通過(guò)把方法亡脑,屬性或下標(biāo)標(biāo)記為final來(lái)防止它們被重寫(xiě)堕澄,只需要在聲明關(guān)鍵字前加上final修飾符即可(例如:final var,final func远豺,final class func奈偏,以及final subscript)。
如果你重寫(xiě)了final方法躯护,屬性或下標(biāo)惊来,在編譯時(shí)會(huì)報(bào)錯(cuò)。在類(lèi)擴(kuò)展中的方法棺滞,屬性或下標(biāo)也可以在擴(kuò)展的定義里標(biāo)記為final的裁蚁。
你可以通過(guò)在關(guān)鍵字class前添加final修飾符(final class)來(lái)將整個(gè)類(lèi)標(biāo)記為final的矢渊。這樣的類(lèi)是不可被繼承的,試圖繼承這樣的類(lèi)會(huì)導(dǎo)致編譯報(bào)錯(cuò)枉证。
88.存儲(chǔ)屬性的初始賦值
類(lèi)和結(jié)構(gòu)體在創(chuàng)建實(shí)例時(shí)矮男,必須為所有存儲(chǔ)型屬性設(shè)置合適的初始值。存儲(chǔ)型屬性的值不能處于一個(gè)未知的狀態(tài)室谚。
當(dāng)你為存儲(chǔ)型屬性設(shè)置默認(rèn)值或者在構(gòu)造器中為其賦值時(shí)揍障,它們的值是被直接設(shè)置的,不會(huì)觸發(fā)任何屬性觀察者(property observers)蚓庭。
89.構(gòu)造器
構(gòu)造器在創(chuàng)建某個(gè)特定類(lèi)型的新實(shí)例時(shí)被調(diào)用痹愚。它的最簡(jiǎn)形式類(lèi)似于一個(gè)不帶任何參數(shù)的實(shí)例方法,以關(guān)鍵字init命名:
init() {
//在此處執(zhí)行構(gòu)造過(guò)程
}
下面例子中定義了一個(gè)用來(lái)保存華氏溫度的結(jié)構(gòu)體Fahrenheit入篮,它擁有一個(gè)Double類(lèi)型的存儲(chǔ)型屬性temperature:
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
//輸出"The default temperature is 32.0° Fahrenheit”
這個(gè)結(jié)構(gòu)體定義了一個(gè)不帶參數(shù)的構(gòu)造器init陈瘦,并在里面將存儲(chǔ)型屬性temperature的值初始化為32.0(華氏溫度下水的冰點(diǎn))。
90.構(gòu)造參數(shù)
自定義構(gòu)造過(guò)程時(shí)潮售,可以在定義中提供構(gòu)造參數(shù)痊项,指定所需值的類(lèi)型和名字。構(gòu)造參數(shù)的功能和語(yǔ)法跟函數(shù)和方法的參數(shù)相同酥诽。
下面例子中定義了一個(gè)包含攝氏度溫度的結(jié)構(gòu)體Celsius鞍泉。它定義了兩個(gè)不同的構(gòu)造器:init(fromFahrenheit:)和init(fromKelvin:),二者分別通過(guò)接受不同溫標(biāo)下的溫度值來(lái)創(chuàng)建新的實(shí)例:
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)
// boilingPointOfWater.temperatureInCelsius是100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius是0.0”
第一個(gè)構(gòu)造器擁有一個(gè)構(gòu)造參數(shù)盆均,其外部名字為fromFahrenheit塞弊,內(nèi)部名字為fahrenheit;第二個(gè)構(gòu)造器也擁有一個(gè)構(gòu)造參數(shù)泪姨,其外部名字為fromKelvin游沿,內(nèi)部名字為kelvin。這兩個(gè)構(gòu)造器都將唯一的參數(shù)值轉(zhuǎn)換成攝氏溫度值肮砾,并保存在屬性temperatureInCelsius中诀黍。
91.不帶外部名的構(gòu)造器參數(shù)
如果你不希望為構(gòu)造器的某個(gè)參數(shù)提供外部名字,你可以使用下劃線(_)來(lái)顯式描述它的外部名仗处,以此重寫(xiě)上面所說(shuō)的默認(rèn)行為眯勾。
下面是之前Celsius例子的擴(kuò)展,跟之前相比添加了一個(gè)帶有Double類(lèi)型參數(shù)的構(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
}
init(_ celsius: Double){
temperatureInCelsius = celsius
}
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius為37.0
調(diào)用Celsius(37.0)意圖明確吃环,不需要外部參數(shù)名稱(chēng)。因此適合使用init(_ celsius: Double)這樣的構(gòu)造器洋幻,從而可以通過(guò)提供Double類(lèi)型的參數(shù)值調(diào)用構(gòu)造器郁轻,而不需要加上外部名。
92.可選屬性類(lèi)型
如果你定制的類(lèi)型包含一個(gè)邏輯上允許取值為空的存儲(chǔ)型屬性——無(wú)論是因?yàn)樗鼰o(wú)法在初始化時(shí)賦值,還是因?yàn)樗谥竽硞€(gè)時(shí)間點(diǎn)可以賦值為空——你都需要將它定義為可選類(lèi)型(optional type)好唯〗吣可選類(lèi)型的屬性將自動(dòng)初始化為nil,表示這個(gè)屬性是有意在初始化時(shí)設(shè)置為空的骑篙。
下面例子中定義了類(lèi)SurveyQuestion蜕提,它包含一個(gè)可選字符串屬性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()
//輸出"Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."
調(diào)查問(wèn)題的答案在回答前是無(wú)法確定的,因此我們將屬性response聲明為String?類(lèi)型靶端,或者說(shuō)是可選字符串類(lèi)型(optional String)谎势。當(dāng)SurveyQuestion實(shí)例化時(shí),它將自動(dòng)賦值為nil躲查,表明此字符串暫時(shí)還沒(méi)有值它浅。
93.構(gòu)造過(guò)程中常量屬性的修改
你可以在構(gòu)造過(guò)程中的任意時(shí)間點(diǎn)給常量屬性指定一個(gè)值,只要在構(gòu)造過(guò)程結(jié)束時(shí)是一個(gè)確定的值镣煮。一旦常量屬性被賦值,它將永遠(yuǎn)不可更改鄙麦。
注意
對(duì)于類(lèi)的實(shí)例來(lái)說(shuō)典唇,它的常量屬性只能在定義它的類(lèi)的構(gòu)造過(guò)程中修改;不能在子類(lèi)中修改胯府。
你可以修改上面的SurveyQuestion示例介衔,用常量屬性替代變量屬性text,表示問(wèn)題內(nèi)容text在SurveyQuestion的實(shí)例被創(chuàng)建之后不會(huì)再被修改骂因。盡管text屬性現(xiàn)在是常量炎咖,我們?nèi)匀豢梢栽陬?lèi)的構(gòu)造器中設(shè)置它的值:
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()
//輸出"How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
94.默認(rèn)構(gòu)造器
如果結(jié)構(gòu)體或類(lèi)的所有屬性都有默認(rèn)值,同時(shí)沒(méi)有自定義的構(gòu)造器寒波,那么Swift會(huì)給這些結(jié)構(gòu)體或類(lèi)提供一個(gè)默認(rèn)構(gòu)造器(default initializers)乘盼。這個(gè)默認(rèn)構(gòu)造器將簡(jiǎn)單地創(chuàng)建一個(gè)所有屬性值都設(shè)置為默認(rèn)值的實(shí)例。
下面例子中創(chuàng)建了一個(gè)類(lèi)ShoppingListItem俄烁,它封裝了購(gòu)物清單中的某一物品的屬性:名字(name)绸栅、數(shù)量(quantity)和購(gòu)買(mǎi)狀態(tài)purchase state:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
由于ShoppingListItem類(lèi)中的所有屬性都有默認(rèn)值,且它是沒(méi)有父類(lèi)的基類(lèi)页屠,它將自動(dòng)獲得一個(gè)可以為所有屬性設(shè)置默認(rèn)值的默認(rèn)構(gòu)造器(盡管代碼中沒(méi)有顯式為name屬性設(shè)置默認(rèn)值粹胯,但由于name是可選字符串類(lèi)型,它將默認(rèn)設(shè)置為nil)辰企。上面例子中使用默認(rèn)構(gòu)造器創(chuàng)造了一個(gè)ShoppingListItem類(lèi)的實(shí)例(使用ShoppingListItem()形式的構(gòu)造器語(yǔ)法)风纠,并將其賦值給變量item。
95.結(jié)構(gòu)體的逐一成員構(gòu)造器
除了上面提到的默認(rèn)構(gòu)造器牢贸,如果結(jié)構(gòu)體沒(méi)有提供自定義的構(gòu)造器竹观,它們將自動(dòng)獲得一個(gè)逐一成員構(gòu)造器,即使結(jié)構(gòu)體的存儲(chǔ)型屬性沒(méi)有默認(rèn)值十减。
逐一成員構(gòu)造器是用來(lái)初始化結(jié)構(gòu)體新實(shí)例里成員屬性的快捷方法栈幸。我們?cè)谡{(diào)用逐一成員構(gòu)造器時(shí)愤估,通過(guò)與成員屬性名相同的參數(shù)名進(jìn)行傳值來(lái)完成對(duì)成員屬性的初始賦值。
下面例子中定義了一個(gè)結(jié)構(gòu)體Size速址,它包含兩個(gè)屬性width和height。Swift可以根據(jù)這兩個(gè)屬性的初始賦值0.0自動(dòng)推導(dǎo)出它們的類(lèi)型為Double芍锚。
結(jié)構(gòu)體Size自動(dòng)獲得了一個(gè)逐一成員構(gòu)造器init(width:height:)昔园。你可以用它來(lái)為Size創(chuàng)建新的實(shí)例:
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
96.值類(lèi)型的構(gòu)造器代理
構(gòu)造器可以通過(guò)調(diào)用其它構(gòu)造器來(lái)完成實(shí)例的部分構(gòu)造過(guò)程。這一過(guò)程稱(chēng)為構(gòu)造器代理并炮,它能減少多個(gè)構(gòu)造器間的代碼重復(fù)默刚。
構(gòu)造器代理的實(shí)現(xiàn)規(guī)則和形式在值類(lèi)型和類(lèi)類(lèi)型中有所不同。值類(lèi)型(結(jié)構(gòu)體和枚舉類(lèi)型)不支持繼承逃魄,所以構(gòu)造器代理的過(guò)程相對(duì)簡(jiǎn)單荤西,因?yàn)樗鼈冎荒艽斫o自己的其它構(gòu)造器。類(lèi)則不同伍俘,它可以繼承自其它類(lèi)(請(qǐng)參考繼承)邪锌,這意味著類(lèi)有責(zé)任保證其所有繼承的存儲(chǔ)型屬性在構(gòu)造時(shí)也能正確的初始化。這些責(zé)任將在后續(xù)章節(jié)類(lèi)的繼承和構(gòu)造過(guò)程中介紹癌瘾。
對(duì)于值類(lèi)型觅丰,你可以使用self.init在自定義的構(gòu)造器中引用類(lèi)型中的其它構(gòu)造器。并且你只能在構(gòu)造器內(nèi)部調(diào)用self.init妨退。
如果你為某個(gè)值類(lèi)型定義了一個(gè)自定義的構(gòu)造器妇萄,你將無(wú)法訪問(wèn)到默認(rèn)構(gòu)造器(如果是結(jié)構(gòu)體,還將無(wú)法訪問(wèn)逐一成員構(gòu)造器)咬荷。這個(gè)限制可以防止你為值類(lèi)型定義了一個(gè)進(jìn)行額外必要設(shè)置的復(fù)雜構(gòu)造器之后愕够,別人還是錯(cuò)誤地使用了一個(gè)自動(dòng)生成的構(gòu)造器疫稿。
注意
假如你希望默認(rèn)構(gòu)造器、逐一成員構(gòu)造器以及你自己的自定義構(gòu)造器都能用來(lái)創(chuàng)建實(shí)例,可以將自定義的構(gòu)造器寫(xiě)到擴(kuò)展(extension)中坏快,而不是寫(xiě)在值類(lèi)型的原始定義中晒衩。想查看更多內(nèi)容增淹,請(qǐng)查看擴(kuò)展章節(jié)膛堤。
下面例子將定義一個(gè)結(jié)構(gòu)體Rect,用來(lái)代表幾何矩形壳影。這個(gè)例子需要兩個(gè)輔助的結(jié)構(gòu)體Size和Point拱层,它們各自為其所有的屬性提供了初始值0.0。
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
你可以通過(guò)以下三種方式為Rect創(chuàng)建實(shí)例——使用被初始化為默認(rèn)值的origin和size屬性來(lái)初始化宴咧;提供指定的origin和size實(shí)例來(lái)初始化根灯;提供指定的center和size來(lái)初始化。在下面Rect結(jié)構(gòu)體定義中,我們?yōu)檫@三種方式提供了三個(gè)自定義的構(gòu)造器:
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è)Rect構(gòu)造器init()烙肺,在功能上跟沒(méi)有自定義構(gòu)造器時(shí)自動(dòng)獲得的默認(rèn)構(gòu)造器是一樣的纳猪。這個(gè)構(gòu)造器是一個(gè)空函數(shù),使用一對(duì)大括號(hào){}來(lái)表示桃笙,它沒(méi)有執(zhí)行任何構(gòu)造過(guò)程氏堤。調(diào)用這個(gè)構(gòu)造器將返回一個(gè)Rect實(shí)例,它的origin和size屬性都使用定義時(shí)的默認(rèn)值Point(x: 0.0, y: 0.0)和Size(width: 0.0, height: 0.0):
let basicRect = Rect()
// basicRect的origin是(0.0, 0.0)搏明,size是(0.0, 0.0)
第二個(gè)Rect構(gòu)造器init(origin:size:)鼠锈,在功能上跟結(jié)構(gòu)體在沒(méi)有自定義構(gòu)造器時(shí)獲得的逐一成員構(gòu)造器是一樣的。這個(gè)構(gòu)造器只是簡(jiǎn)單地將origin和size的參數(shù)值賦給對(duì)應(yīng)的存儲(chǔ)型屬性:
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// originRect的origin是(2.0, 2.0)星著,size是(5.0, 5.0)
第三個(gè)Rect構(gòu)造器init(center:size:)稍微復(fù)雜一點(diǎn)购笆。它先通過(guò)center和size的值計(jì)算出origin的坐標(biāo),然后再調(diào)用(或者說(shuō)代理給)init(origin:size:)構(gòu)造器來(lái)將新的origin和size值賦值到對(duì)應(yīng)的屬性中:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect的origin是(2.5, 2.5)虚循,size是(3.0, 3.0)
構(gòu)造器init(center:size:)可以直接將origin和size的新值賦值到對(duì)應(yīng)的屬性中同欠。然而,利用恰好提供了相關(guān)功能的現(xiàn)有構(gòu)造器會(huì)更為方便邮丰,構(gòu)造器init(center:size:)的意圖也會(huì)更加清晰行您。
注意
如果你想用另外一種不需要自己定義init()和init(origin:size:)的方式來(lái)實(shí)現(xiàn)這個(gè)例子
97.指定構(gòu)造器和便利構(gòu)造器的語(yǔ)法
類(lèi)的指定構(gòu)造器的寫(xiě)法跟值類(lèi)型簡(jiǎn)單構(gòu)造器一樣:
init(parameters) {
statements
}
便利構(gòu)造器也采用相同樣式的寫(xiě)法,但需要在init關(guān)鍵字之前放置convenience關(guān)鍵字剪廉,并使用空格將它們倆分開(kāi):
convenience init(parameters) {
statements
}
98.類(lèi)的構(gòu)造器代理規(guī)則
為了簡(jiǎn)化指定構(gòu)造器和便利構(gòu)造器之間的調(diào)用關(guān)系,Swift采用以下三條規(guī)則來(lái)限制構(gòu)造器之間的代理調(diào)用:
規(guī)則1
指定構(gòu)造器必須調(diào)用其直接父類(lèi)的的指定構(gòu)造器炕檩。
規(guī)則2
便利構(gòu)造器必須調(diào)用同一類(lèi)中定義的其它構(gòu)造器斗蒋。
規(guī)則3
便利構(gòu)造器必須最終導(dǎo)致一個(gè)指定構(gòu)造器被調(diào)用。
一個(gè)更方便記憶的方法是:
指定構(gòu)造器必須總是向上代理
便利構(gòu)造器必須總是橫向代理
這些規(guī)則可以通過(guò)下面圖例來(lái)說(shuō)明:
如圖所示笛质,父類(lèi)中包含一個(gè)指定構(gòu)造器和兩個(gè)便利構(gòu)造器泉沾。其中一個(gè)便利構(gòu)造器調(diào)用了另外一個(gè)便利構(gòu)造器,而后者又調(diào)用了唯一的指定構(gòu)造器妇押。這滿足了上面提到的規(guī)則2和3跷究。這個(gè)父類(lèi)沒(méi)有自己的父類(lèi),所以規(guī)則1沒(méi)有用到敲霍。
子類(lèi)中包含兩個(gè)指定構(gòu)造器和一個(gè)便利構(gòu)造器俊马。便利構(gòu)造器必須調(diào)用兩個(gè)指定構(gòu)造器中的任意一個(gè),因?yàn)樗荒苷{(diào)用同一個(gè)類(lèi)里的其他構(gòu)造器肩杈。這滿足了上面提到的規(guī)則2和3柴我。而兩個(gè)指定構(gòu)造器必須調(diào)用父類(lèi)中唯一的指定構(gòu)造器,這滿足了規(guī)則1扩然。
注意
這些規(guī)則不會(huì)影響類(lèi)的實(shí)例如何創(chuàng)建艘儒。任何上圖中展示的構(gòu)造器都可以用來(lái)創(chuàng)建完全初始化的實(shí)例。這些規(guī)則只影響類(lèi)定義如何實(shí)現(xiàn)。
下面圖例中展示了一種涉及四個(gè)類(lèi)的更復(fù)雜的類(lèi)層級(jí)結(jié)構(gòu)界睁。它演示了指定構(gòu)造器是如何在類(lèi)層級(jí)中充當(dāng)“管道”的作用觉增,在類(lèi)的構(gòu)造器鏈上簡(jiǎn)化了類(lèi)之間的相互關(guān)系。
兩段式構(gòu)造過(guò)程
Swift中類(lèi)的構(gòu)造過(guò)程包含兩個(gè)階段翻斟。第一個(gè)階段逾礁,每個(gè)存儲(chǔ)型屬性被引入它們的類(lèi)指定一個(gè)初始值。當(dāng)每個(gè)存儲(chǔ)型屬性的初始值被確定后杨赤,第二階段開(kāi)始敞斋,它給每個(gè)類(lèi)一次機(jī)會(huì),在新實(shí)例準(zhǔn)備使用之前進(jìn)一步定制它們的存儲(chǔ)型屬性疾牲。
兩段式構(gòu)造過(guò)程的使用讓構(gòu)造過(guò)程更安全植捎,同時(shí)在整個(gè)類(lèi)層級(jí)結(jié)構(gòu)中給予了每個(gè)類(lèi)完全的靈活性。兩段式構(gòu)造過(guò)程可以防止屬性值在初始化之前被訪問(wèn)阳柔,也可以防止屬性被另外一個(gè)構(gòu)造器意外地賦予不同的值焰枢。
注意
Swift的兩段式構(gòu)造過(guò)程跟Objective-C中的構(gòu)造過(guò)程類(lèi)似。最主要的區(qū)別在于階段1舌剂,Objective-C給每一個(gè)屬性賦值0或空值(比如說(shuō)0或nil)济锄。Swift的構(gòu)造流程則更加靈活,它允許你設(shè)置定制的初始值霍转,并自如應(yīng)對(duì)某些屬性不能以0或nil作為合法默認(rèn)值的情況荐绝。
Swift編譯器將執(zhí)行4種有效的安全檢查,以確保兩段式構(gòu)造過(guò)程能不出錯(cuò)地完成:
安全檢查1
指定構(gòu)造器必須保證它所在類(lèi)引入的所有屬性都必須先初始化完成避消,之后才能將其它構(gòu)造任務(wù)向上代理給父類(lèi)中的構(gòu)造器低滩。
如上所述,一個(gè)對(duì)象的內(nèi)存只有在其所有存儲(chǔ)型屬性確定之后才能完全初始化岩喷。為了滿足這一規(guī)則恕沫,指定構(gòu)造器必須保證它所在類(lèi)引入的屬性在它往上代理之前先完成初始化。
安全檢查2
指定構(gòu)造器必須先向上代理調(diào)用父類(lèi)構(gòu)造器纱意,然后再為繼承的屬性設(shè)置新值婶溯。如果沒(méi)這么做,指定構(gòu)造器賦予的新值將被父類(lèi)中的構(gòu)造器所覆蓋偷霉。
安全檢查3
便利構(gòu)造器必須先代理調(diào)用同一類(lèi)中的其它構(gòu)造器迄委,然后再為任意屬性賦新值。如果沒(méi)這么做腾它,便利構(gòu)造器賦予的新值將被同一類(lèi)中其它指定構(gòu)造器所覆蓋跑筝。
安全檢查4
構(gòu)造器在第一階段構(gòu)造完成之前,不能調(diào)用任何實(shí)例方法瞒滴,不能讀取任何實(shí)例屬性的值曲梗,不能引用self作為一個(gè)值赞警。
類(lèi)實(shí)例在第一階段結(jié)束以前并不是完全有效的。只有第一階段完成后虏两,該實(shí)例才會(huì)成為有效實(shí)例愧旦,才能訪問(wèn)屬性和調(diào)用方法。
以下是兩段式構(gòu)造過(guò)程中基于上述安全檢查的構(gòu)造流程展示:
階段1
某個(gè)指定構(gòu)造器或便利構(gòu)造器被調(diào)用定罢。
完成新實(shí)例內(nèi)存的分配笤虫,但此時(shí)內(nèi)存還沒(méi)有被初始化。
指定構(gòu)造器確保其所在類(lèi)引入的所有存儲(chǔ)型屬性都已賦初值祖凫。存儲(chǔ)型屬性所屬的內(nèi)存完成初始化琼蚯。
指定構(gòu)造器將調(diào)用父類(lèi)的構(gòu)造器,完成父類(lèi)屬性的初始化惠况。
這個(gè)調(diào)用父類(lèi)構(gòu)造器的過(guò)程沿著構(gòu)造器鏈一直往上執(zhí)行遭庶,直到到達(dá)構(gòu)造器鏈的最頂部。
當(dāng)?shù)竭_(dá)了構(gòu)造器鏈最頂部稠屠,且已確保所有實(shí)例包含的存儲(chǔ)型屬性都已經(jīng)賦值峦睡,這個(gè)實(shí)例的內(nèi)存被認(rèn)為已經(jīng)完全初始化。此時(shí)階段1完成权埠。
階段2
從頂部構(gòu)造器鏈一直往下榨了,每個(gè)構(gòu)造器鏈中類(lèi)的指定構(gòu)造器都有機(jī)會(huì)進(jìn)一步定制實(shí)例。構(gòu)造器此時(shí)可以訪問(wèn)self攘蔽、修改它的屬性并調(diào)用實(shí)例方法等等遵湖。
最終渡蜻,任意構(gòu)造器鏈中的便利構(gòu)造器可以有機(jī)會(huì)定制實(shí)例和使用self碟渺。
下圖展示了在假定的子類(lèi)和父類(lèi)之間的構(gòu)造階段1:
在這個(gè)例子中览徒,構(gòu)造過(guò)程從對(duì)子類(lèi)中一個(gè)便利構(gòu)造器的調(diào)用開(kāi)始。這個(gè)便利構(gòu)造器此時(shí)沒(méi)法修改任何屬性漫雷,它把構(gòu)造任務(wù)代理給同一類(lèi)中的指定構(gòu)造器。
如安全檢查1所示鳍咱,指定構(gòu)造器將確保所有子類(lèi)的屬性都有值降盹。然后它將調(diào)用父類(lèi)的指定構(gòu)造器,并沿著構(gòu)造器鏈一直往上完成父類(lèi)的構(gòu)造過(guò)程谤辜。
父類(lèi)中的指定構(gòu)造器確保所有父類(lèi)的屬性都有值蓄坏。由于沒(méi)有更多的父類(lèi)需要初始化,也就無(wú)需繼續(xù)向上代理丑念。
一旦父類(lèi)中所有屬性都有了初始值涡戳,實(shí)例的內(nèi)存被認(rèn)為是完全初始化,階段1完成脯倚。
以下展示了相同構(gòu)造過(guò)程的階段2:
父類(lèi)中的指定構(gòu)造器現(xiàn)在有機(jī)會(huì)進(jìn)一步來(lái)定制實(shí)例(盡管這不是必須的)渔彰。
一旦父類(lèi)中的指定構(gòu)造器完成調(diào)用嵌屎,子類(lèi)中的指定構(gòu)造器可以執(zhí)行更多的定制操作(這也不是必須的)。
最終恍涂,一旦子類(lèi)的指定構(gòu)造器完成調(diào)用宝惰,最開(kāi)始被調(diào)用的便利構(gòu)造器可以執(zhí)行更多的定制操作。
99.構(gòu)造器的繼承和重寫(xiě)
跟Objective-C中的子類(lèi)不同再沧,Swift中的子類(lèi)默認(rèn)情況下不會(huì)繼承父類(lèi)的構(gòu)造器尼夺。Swift的這種機(jī)制可以防止一個(gè)父類(lèi)的簡(jiǎn)單構(gòu)造器被一個(gè)更專(zhuān)業(yè)的子類(lèi)繼承,并被錯(cuò)誤地用來(lái)創(chuàng)建子類(lèi)的實(shí)例炒瘸。
注意
父類(lèi)的構(gòu)造器僅會(huì)在安全和適當(dāng)?shù)那闆r下被繼承淤堵。具體內(nèi)容請(qǐng)參考后續(xù)章節(jié)構(gòu)造器的自動(dòng)繼承。
假如你希望自定義的子類(lèi)中能提供一個(gè)或多個(gè)跟父類(lèi)相同的構(gòu)造器顷扩,你可以在子類(lèi)中提供這些構(gòu)造器的自定義實(shí)現(xiàn)拐邪。
當(dāng)你在編寫(xiě)一個(gè)和父類(lèi)中指定構(gòu)造器相匹配的子類(lèi)構(gòu)造器時(shí),你實(shí)際上是在重寫(xiě)父類(lèi)的這個(gè)指定構(gòu)造器屎即。因此庙睡,你必須在定義子類(lèi)構(gòu)造器時(shí)帶上override修飾符。即使你重寫(xiě)的是系統(tǒng)自動(dòng)提供的默認(rèn)構(gòu)造器技俐,也需要帶上override修飾符乘陪,具體內(nèi)容請(qǐng)參考默認(rèn)構(gòu)造器。
正如重寫(xiě)屬性雕擂,方法或者是下標(biāo)啡邑,override修飾符會(huì)讓編譯器去檢查父類(lèi)中是否有相匹配的指定構(gòu)造器,并驗(yàn)證構(gòu)造器參數(shù)是否正確井赌。
注意
當(dāng)你重寫(xiě)一個(gè)父類(lèi)的指定構(gòu)造器時(shí)谤逼,你總是需要寫(xiě)override修飾符,即使你的子類(lèi)將父類(lèi)的指定構(gòu)造器重寫(xiě)為了便利構(gòu)造器仇穗。
相反流部,如果你編寫(xiě)了一個(gè)和父類(lèi)便利構(gòu)造器相匹配的子類(lèi)構(gòu)造器,由于子類(lèi)不能直接調(diào)用父類(lèi)的便利構(gòu)造器(每個(gè)規(guī)則都在上文類(lèi)的構(gòu)造器代理規(guī)則有所描述)纹坐,因此枝冀,嚴(yán)格意義上來(lái)講,你的子類(lèi)并未對(duì)一個(gè)父類(lèi)構(gòu)造器提供重寫(xiě)耘子。最后的結(jié)果就是果漾,你在子類(lèi)中“重寫(xiě)”一個(gè)父類(lèi)便利構(gòu)造器時(shí),不需要加override前綴谷誓。
在下面的例子中定義了一個(gè)叫Vehicle的基類(lèi)绒障。基類(lèi)中聲明了一個(gè)存儲(chǔ)型屬性numberOfWheels捍歪,它是值為0的Int類(lèi)型的存儲(chǔ)型屬性户辱。numberOfWheels屬性用于創(chuàng)建名為descrpiption的String類(lèi)型的計(jì)算型屬性:
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
Vehicle類(lèi)只為存儲(chǔ)型屬性提供默認(rèn)值鸵钝,而不自定義構(gòu)造器。因此焕妙,它會(huì)自動(dòng)獲得一個(gè)默認(rèn)構(gòu)造器蒋伦,具體內(nèi)容請(qǐng)參考默認(rèn)構(gòu)造器。自動(dòng)獲得的默認(rèn)構(gòu)造器總會(huì)是類(lèi)中的指定構(gòu)造器焚鹊,它可以用于創(chuàng)建numberOfWheels為0的Vehicle實(shí)例:
let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)
下面例子中定義了一個(gè)Vehicle的子類(lèi)Bicycle:
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
子類(lèi)Bicycle定義了一個(gè)自定義指定構(gòu)造器init()痕届。這個(gè)指定構(gòu)造器和父類(lèi)的指定構(gòu)造器相匹配,所以Bicycle中的指定構(gòu)造器需要帶上override修飾符末患。
Bicycle的構(gòu)造器init()以調(diào)用super.init()方法開(kāi)始研叫,這個(gè)方法的作用是調(diào)用Bicycle的父類(lèi)Vehicle的默認(rèn)構(gòu)造器。這樣可以確保Bicycle在修改屬性之前璧针,它所繼承的屬性numberOfWheels能被Vehicle類(lèi)初始化嚷炉。在調(diào)用super.init()之后,屬性numberOfWheels的原值被新值2替換探橱。
如果你創(chuàng)建一個(gè)Bicycle實(shí)例申屹,你可以調(diào)用繼承的description計(jì)算型屬性去查看屬性numberOfWheels是否有改變:
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)
注意
子類(lèi)可以在初始化時(shí)修改繼承來(lái)的變量屬性,但是不能修改繼承來(lái)的常量屬性隧膏。
100.構(gòu)造器的自動(dòng)繼承
如上所述哗讥,子類(lèi)在默認(rèn)情況下不會(huì)繼承父類(lèi)的構(gòu)造器。但是如果滿足特定條件胞枕,父類(lèi)構(gòu)造器是可以被自動(dòng)繼承的杆煞。在實(shí)踐中,這意味著對(duì)于許多常見(jiàn)場(chǎng)景你不必重寫(xiě)父類(lèi)的構(gòu)造器腐泻,并且可以在安全的情況下以最小的代價(jià)繼承父類(lèi)的構(gòu)造器决乎。
假設(shè)你為子類(lèi)中引入的所有新屬性都提供了默認(rèn)值,以下2個(gè)規(guī)則適用:
規(guī)則1
如果子類(lèi)沒(méi)有定義任何指定構(gòu)造器派桩,它將自動(dòng)繼承所有父類(lèi)的指定構(gòu)造器构诚。
規(guī)則2
如果子類(lèi)提供了所有父類(lèi)指定構(gòu)造器的實(shí)現(xiàn)——無(wú)論是通過(guò)規(guī)則1繼承過(guò)來(lái)的,還是提供了自定義實(shí)現(xiàn)——它將自動(dòng)繼承所有父類(lèi)的便利構(gòu)造器铆惑。
即使你在子類(lèi)中添加了更多的便利構(gòu)造器唤反,這兩條規(guī)則仍然適用。
注意
對(duì)于規(guī)則2鸭津,子類(lèi)可以將父類(lèi)的指定構(gòu)造器實(shí)現(xiàn)為便利構(gòu)造器。
指定構(gòu)造器和便利構(gòu)造器實(shí)踐
接下來(lái)的例子將在實(shí)踐中展示指定構(gòu)造器肠缨、便利構(gòu)造器以及構(gòu)造器的自動(dòng)繼承逆趋。這個(gè)例子定義了包含三個(gè)類(lèi)Food、RecipeIngredient以及ShoppingListItem的類(lèi)層次結(jié)構(gòu)晒奕,并將演示它們的構(gòu)造器是如何相互作用的闻书。
類(lèi)層次中的基類(lèi)是Food名斟,它是一個(gè)簡(jiǎn)單的用來(lái)封裝食物名字的類(lèi)。Food類(lèi)引入了一個(gè)叫做name的String類(lèi)型的屬性魄眉,并且提供了兩個(gè)構(gòu)造器來(lái)創(chuàng)建Food實(shí)例:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
下圖中展示了Food的構(gòu)造器鏈:
類(lèi)類(lèi)型沒(méi)有默認(rèn)的逐一成員構(gòu)造器砰盐,所以Food類(lèi)提供了一個(gè)接受單一參數(shù)name的指定構(gòu)造器。這個(gè)構(gòu)造器可以使用一個(gè)特定的名字來(lái)創(chuàng)建新的Food實(shí)例:
let namedMeat = Food(name: "Bacon")
// namedMeat的名字是"Bacon”
Food類(lèi)中的構(gòu)造器init(name: String)被定義為一個(gè)指定構(gòu)造器坑律,因?yàn)樗艽_保Food實(shí)例的所有存儲(chǔ)型屬性都被初始化岩梳。Food類(lèi)沒(méi)有父類(lèi),所以init(name: String)構(gòu)造器不需要調(diào)用super.init()來(lái)完成構(gòu)造過(guò)程晃择。
Food類(lèi)同樣提供了一個(gè)沒(méi)有參數(shù)的便利構(gòu)造器init()冀值。這個(gè)init()構(gòu)造器為新食物提供了一個(gè)默認(rèn)的占位名字,通過(guò)橫向代理到指定構(gòu)造器init(name: String)并給參數(shù)name傳值[Unnamed]來(lái)實(shí)現(xiàn):
let mysteryMeat = Food()
// mysteryMeat的名字是[Unnamed]
類(lèi)層級(jí)中的第二個(gè)類(lèi)是Food的子類(lèi)RecipeIngredient宫屠。RecipeIngredient類(lèi)構(gòu)建了食譜中的一味調(diào)味劑列疗。它引入了Int類(lèi)型的屬性quantity(以及從Food繼承過(guò)來(lái)的name屬性),并且定義了兩個(gè)構(gòu)造器來(lái)創(chuàng)建RecipeIngredient實(shí)例:
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類(lèi)的構(gòu)造器鏈:
RecipeIngredient類(lèi)擁有一個(gè)指定構(gòu)造器init(name: String, quantity: Int)浪蹂,它可以用來(lái)填充RecipeIngredient實(shí)例的所有屬性值抵栈。這個(gè)構(gòu)造器一開(kāi)始先將傳入的quantity參數(shù)賦值給quantity屬性颈渊,這個(gè)屬性也是唯一在RecipeIngredient中新引入的屬性诅诱。隨后取董,構(gòu)造器向上代理到父類(lèi)Food的init(name: String)如失。這個(gè)過(guò)程滿足兩段式構(gòu)造過(guò)程中的安全檢查1敬飒。
RecipeIngredient還定義了一個(gè)便利構(gòu)造器init(name: String)勋眯,它只通過(guò)name來(lái)創(chuàng)建RecipeIngredient的實(shí)例值朋。這個(gè)便利構(gòu)造器假設(shè)任意RecipeIngredient實(shí)例的quantity為1沮稚,所以不需要顯式指明數(shù)量即可創(chuàng)建出實(shí)例洛波。這個(gè)便利構(gòu)造器的定義可以更加方便和快捷地創(chuàng)建實(shí)例胰舆,并且避免了創(chuàng)建多個(gè)quantity為1的RecipeIngredient實(shí)例時(shí)的代碼重復(fù)。這個(gè)便利構(gòu)造器只是簡(jiǎn)單地橫向代理到類(lèi)中的指定構(gòu)造器蹬挤,并為quantity參數(shù)傳遞1缚窿。
注意,RecipeIngredient的便利構(gòu)造器init(name: String)使用了跟Food中指定構(gòu)造器init(name: String)相同的參數(shù)焰扳。由于這個(gè)便利構(gòu)造器重寫(xiě)了父類(lèi)的指定構(gòu)造器init(name: String)倦零,因此必須在前面使用override修飾符(參見(jiàn)構(gòu)造器的繼承和重寫(xiě))。
盡管RecipeIngredient將父類(lèi)的指定構(gòu)造器重寫(xiě)為了便利構(gòu)造器吨悍,它依然提供了父類(lèi)的所有指定構(gòu)造器的實(shí)現(xiàn)扫茅。因此,RecipeIngredient會(huì)自動(dòng)繼承父類(lèi)的所有便利構(gòu)造器育瓜。
在這個(gè)例子中葫隙,RecipeIngredient的父類(lèi)是Food,它有一個(gè)便利構(gòu)造器init()躏仇。這個(gè)便利構(gòu)造器會(huì)被RecipeIngredient繼承恋脚。這個(gè)繼承版本的init()在功能上跟Food提供的版本是一樣的腺办,只是它會(huì)代理到RecipeIngredient版本的init(name: String)而不是Food提供的版本。
所有的這三種構(gòu)造器都可以用來(lái)創(chuàng)建新的RecipeIngredient實(shí)例:
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
類(lèi)層級(jí)中第三個(gè)也是最后一個(gè)類(lèi)是RecipeIngredient的子類(lèi)糟描,叫做ShoppingListItem怀喉。這個(gè)類(lèi)構(gòu)建了購(gòu)物單中出現(xiàn)的某一種調(diào)味料。
購(gòu)物單中的每一項(xiàng)總是從未購(gòu)買(mǎi)狀態(tài)開(kāi)始的船响。為了呈現(xiàn)這一事實(shí)躬拢,ShoppingListItem引入了一個(gè)布爾類(lèi)型的屬性purchased,它的默認(rèn)值是false灿意。ShoppingListItem還添加了一個(gè)計(jì)算型屬性description估灿,它提供了關(guān)于ShoppingListItem實(shí)例的一些文字描述:
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? "?" : "?"
return output
}
}
注意
ShoppingListItem沒(méi)有定義構(gòu)造器來(lái)為purchased提供初始值,因?yàn)樘砑拥劫?gòu)物單的物品的初始狀態(tài)總是未購(gòu)買(mǎi)缤剧。
由于它為自己引入的所有屬性都提供了默認(rèn)值馅袁,并且自己沒(méi)有定義任何構(gòu)造器,ShoppingListItem將自動(dòng)繼承所有父類(lèi)中的指定構(gòu)造器和便利構(gòu)造器荒辕。
下圖展示了這三個(gè)類(lèi)的構(gòu)造器鏈:
你可以使用全部三個(gè)繼承來(lái)的構(gòu)造器來(lái)創(chuàng)建ShoppingListItem的新實(shí)例:
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?
如上所述汗销,例子中通過(guò)字面量方式創(chuàng)建了一個(gè)數(shù)組breakfastList,它包含了三個(gè)ShoppingListItem實(shí)例抵窒,因此數(shù)組的類(lèi)型也能被自動(dòng)推導(dǎo)為[ShoppingListItem]弛针。在數(shù)組創(chuàng)建完之后,數(shù)組中第一個(gè)ShoppingListItem實(shí)例的名字從[Unnamed]更改為Orange juice李皇,并標(biāo)記為已購(gòu)買(mǎi)削茁。打印數(shù)組中每個(gè)元素的描述顯示了它們都已按照預(yù)期被賦值。
101.可失敗構(gòu)造器
如果一個(gè)類(lèi)掉房、結(jié)構(gòu)體或枚舉類(lèi)型的對(duì)象茧跋,在構(gòu)造過(guò)程中有可能失敗,則為其定義一個(gè)可失敗構(gòu)造器卓囚。這里所指的“失敗”是指瘾杭,如給構(gòu)造器傳入無(wú)效的參數(shù)值,或缺少某種所需的外部資源哪亿,又或是不滿足某種必要的條件等粥烁。
為了妥善處理這種構(gòu)造過(guò)程中可能會(huì)失敗的情況。你可以在一個(gè)類(lèi)蝇棉,結(jié)構(gòu)體或是枚舉類(lèi)型的定義中讨阻,添加一個(gè)或多個(gè)可失敗構(gòu)造器。其語(yǔ)法為在init關(guān)鍵字后面添加問(wèn)號(hào)(init?)篡殷。
注意
可失敗構(gòu)造器的參數(shù)名和參數(shù)類(lèi)型变勇,不能與其它非可失敗構(gòu)造器的參數(shù)名,及其參數(shù)類(lèi)型相同。
可失敗構(gòu)造器會(huì)創(chuàng)建一個(gè)類(lèi)型為自身類(lèi)型的可選類(lèi)型的對(duì)象搀绣。你通過(guò)return nil語(yǔ)句來(lái)表明可失敗構(gòu)造器在何種情況下應(yīng)該“失敗”。
注意
嚴(yán)格來(lái)說(shuō)戳气,構(gòu)造器都不支持返回值链患。因?yàn)闃?gòu)造器本身的作用,只是為了確保對(duì)象能被正確構(gòu)造瓶您。因此你只是用return nil表明可失敗構(gòu)造器構(gòu)造失敗麻捻,而不要用關(guān)鍵字return來(lái)表明構(gòu)造成功。
下例中呀袱,定義了一個(gè)名為Animal的結(jié)構(gòu)體贸毕,其中有一個(gè)名為species的String類(lèi)型的常量屬性。同時(shí)該結(jié)構(gòu)體還定義了一個(gè)接受一個(gè)名為species的String類(lèi)型參數(shù)的可失敗構(gòu)造器夜赵。這個(gè)可失敗構(gòu)造器檢查傳入的參數(shù)是否為一個(gè)空字符串明棍。如果為空字符串,則構(gòu)造失敗寇僧。否則摊腋,species屬性被賦值,構(gòu)造成功嘁傀。
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
你可以通過(guò)該可失敗構(gòu)造器來(lái)構(gòu)建一個(gè)Animal的實(shí)例兴蒸,并檢查構(gòu)造過(guò)程是否成功:
let someCreature = Animal(species: "Giraffe")
// someCreature的類(lèi)型是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"
如果你給該可失敗構(gòu)造器傳入一個(gè)空字符串作為其參數(shù),則會(huì)導(dǎo)致構(gòu)造失斚赴臁:
let anonymousCreature = Animal(species: "")
// anonymousCreature的類(lèi)型是Animal?,而不是Animal
if anonymousCreature == nil {
print("The anonymous creature could not be initialized")
}
//打印"The anonymous creature could not be initialized"
注意
空字符串(如""橙凳,而不是"Giraffe")和一個(gè)值為nil的可選類(lèi)型的字符串是兩個(gè)完全不同的概念。上例中的空字符串("")其實(shí)是一個(gè)有效的笑撞,非可選類(lèi)型的字符串岛啸。這里我們之所以讓Animal的可失敗構(gòu)造器構(gòu)造失敗,只是因?yàn)閷?duì)于Animal這個(gè)類(lèi)的species屬性來(lái)說(shuō)娃殖,它更適合有一個(gè)具體的值值戳,而不是空字符串。
102.帶原始值的枚舉類(lèi)型的可失敗構(gòu)造器
帶原始值的枚舉類(lèi)型會(huì)自帶一個(gè)可失敗構(gòu)造器init?(rawValue:)炉爆,該可失敗構(gòu)造器有一個(gè)名為rawValue的參數(shù)堕虹,其類(lèi)型和枚舉類(lèi)型的原始值類(lèi)型一致,如果該參數(shù)的值能夠和某個(gè)枚舉成員的原始值匹配芬首,則該構(gòu)造器會(huì)構(gòu)造相應(yīng)的枚舉成員赴捞,否則構(gòu)造失敗。
因此上面的TemperatureUnit的例子可以重寫(xiě)為:
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."
103.構(gòu)造失敗的傳遞
類(lèi)郁稍,結(jié)構(gòu)體赦政,枚舉的可失敗構(gòu)造器可以橫向代理到類(lèi)型中的其他可失敗構(gòu)造器。類(lèi)似的,子類(lèi)的可失敗構(gòu)造器也能向上代理到父類(lèi)的可失敗構(gòu)造器恢着。
無(wú)論是向上代理還是橫向代理桐愉,如果你代理到的其他可失敗構(gòu)造器觸發(fā)構(gòu)造失敗,整個(gè)構(gòu)造過(guò)程將立即終止掰派,接下來(lái)的任何構(gòu)造代碼不會(huì)再被執(zhí)行从诲。
注意
可失敗構(gòu)造器也可以代理到其它的非可失敗構(gòu)造器。通過(guò)這種方式靡羡,你可以增加一個(gè)可能的失敗狀態(tài)到現(xiàn)有的構(gòu)造過(guò)程中系洛。
下面這個(gè)例子,定義了一個(gè)名為CartItem的Product類(lèi)的子類(lèi)略步。這個(gè)類(lèi)建立了一個(gè)在線購(gòu)物車(chē)中的物品的模型描扯,它有一個(gè)名為quantity的常量存儲(chǔ)型屬性,并確保該屬性的值至少為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可失敗構(gòu)造器首先驗(yàn)證接收的quantity值是否大于等于1趟薄。倘若quantity值無(wú)效绽诚,則立即終止整個(gè)構(gòu)造過(guò)程帆竹,返回失敗結(jié)果茅逮,且不再執(zhí)行余下代碼。同樣地德绿,Product的可失敗構(gòu)造器首先檢查name值岔帽,假如name值為空字符串玫鸟,則構(gòu)造器立即執(zhí)行失敗。
如果你通過(guò)傳入一個(gè)非空字符串name以及一個(gè)值大于等于1的quantity來(lái)創(chuàng)建一個(gè)CartItem實(shí)例犀勒,那么構(gòu)造方法能夠成功被執(zhí)行:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
//打印"Item: sock, quantity: 2”
倘若你以一個(gè)值為0的quantity來(lái)創(chuàng)建一個(gè)CartItem實(shí)例屎飘,那么將導(dǎo)致CartItem構(gòu)造器失敗:
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”
同樣地贾费,如果你嘗試傳入一個(gè)值為空字符串的name來(lái)創(chuàng)建一個(gè)CartItem實(shí)例钦购,那么將導(dǎo)致父類(lèi)Product的構(gòu)造過(guò)程失敗:
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”
104.重寫(xiě)一個(gè)可失敗構(gòu)造器
如同其它的構(gòu)造器褂萧,你可以在子類(lèi)中重寫(xiě)父類(lèi)的可失敗構(gòu)造器押桃。或者你也可以用子類(lèi)的非可失敗構(gòu)造器重寫(xiě)一個(gè)父類(lèi)的可失敗構(gòu)造器导犹。這使你可以定義一個(gè)不會(huì)構(gòu)造失敗的子類(lèi)唱凯,即使父類(lèi)的構(gòu)造器允許構(gòu)造失敗。
注意谎痢,當(dāng)你用子類(lèi)的非可失敗構(gòu)造器重寫(xiě)父類(lèi)的可失敗構(gòu)造器時(shí)磕昼,向上代理到父類(lèi)的可失敗構(gòu)造器的唯一方式是對(duì)父類(lèi)的可失敗構(gòu)造器的返回值進(jìn)行強(qiáng)制解包。
注意
你可以用非可失敗構(gòu)造器重寫(xiě)可失敗構(gòu)造器节猿,但反過(guò)來(lái)卻不行票从。
下例定義了一個(gè)名為Document的類(lèi),name屬性的值必須為一個(gè)非空字符串或nil,但不能是一個(gè)空字符串:
class Document {
var name: String?
//該構(gòu)造器創(chuàng)建了一個(gè)name屬性的值為nil的document實(shí)例
init() {}
//該構(gòu)造器創(chuàng)建了一個(gè)name屬性的值為非空字符串的document實(shí)例
init?(name: String) {
self.name = name
if name.isEmpty { return nil }
}
}
下面這個(gè)例子峰鄙,定義了一個(gè)Document類(lèi)的子類(lèi)AutomaticallyNamedDocument浸间。這個(gè)子類(lèi)重寫(xiě)了父類(lèi)的兩個(gè)指定構(gòu)造器,確保了無(wú)論是使用init()構(gòu)造器吟榴,還是使用init(name:)構(gòu)造器并為參數(shù)傳遞空字符串发框,生成的實(shí)例中的name屬性總有初始"[Untitled]":
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用一個(gè)非可失敗構(gòu)造器init(name:)重寫(xiě)了父類(lèi)的可失敗構(gòu)造器init?(name:)。因?yàn)樽宇?lèi)用另一種方式處理了空字符串的情況煤墙,所以不再需要一個(gè)可失敗構(gòu)造器,因此子類(lèi)用一個(gè)非可失敗構(gòu)造器代替了父類(lèi)的可失敗構(gòu)造器宪拥。
你可以在子類(lèi)的非可失敗構(gòu)造器中使用強(qiáng)制解包來(lái)調(diào)用父類(lèi)的可失敗構(gòu)造器仿野。比如,下面的UntitledDocument子類(lèi)的name屬性的值總是"[Untitled]"她君,它在構(gòu)造過(guò)程中使用了父類(lèi)的可失敗構(gòu)造器init?(name:):
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
在這個(gè)例子中脚作,如果在調(diào)用父類(lèi)的可失敗構(gòu)造器init?(name:)時(shí)傳入的是空字符串,那么強(qiáng)制解包操作會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤缔刹。不過(guò)球涛,因?yàn)檫@里是通過(guò)非空的字符串常量來(lái)調(diào)用它,所以并不會(huì)發(fā)生運(yùn)行時(shí)錯(cuò)誤校镐。
105.可失敗構(gòu)造器init!
通常來(lái)說(shuō)我們通過(guò)在init關(guān)鍵字后添加問(wèn)號(hào)的方式(init?)來(lái)定義一個(gè)可失敗構(gòu)造器亿扁,但你也可以通過(guò)在init后面添加驚嘆號(hào)的方式來(lái)定義一個(gè)可失敗構(gòu)造器((init!)),該可失敗構(gòu)造器將會(huì)構(gòu)建一個(gè)對(duì)應(yīng)類(lèi)型的隱式解包可選類(lèi)型的對(duì)象鸟廓。
你可以在init?中代理到init!从祝,反之亦然。你也可以用init?重寫(xiě)init!引谜,反之亦然牍陌。你還可以用init代理到init!,不過(guò)员咽,一旦init!構(gòu)造失敗毒涧,則會(huì)觸發(fā)一個(gè)斷言。
106.必要構(gòu)造器
在類(lèi)的構(gòu)造器前添加required修飾符表明所有該類(lèi)的子類(lèi)都必須實(shí)現(xiàn)該構(gòu)造器:
class SomeClass {
required init() {
//構(gòu)造器的實(shí)現(xiàn)代碼
}
}
在子類(lèi)重寫(xiě)父類(lèi)的必要構(gòu)造器時(shí)契讲,必須在子類(lèi)的構(gòu)造器前也添加required修飾符尖阔,表明該構(gòu)造器要求也應(yīng)用于繼承鏈后面的子類(lèi)萍诱。在重寫(xiě)父類(lèi)中必要的指定構(gòu)造器時(shí)慰照,不需要添加override修飾符:
class SomeSubclass: SomeClass {
required init() {
//構(gòu)造器的實(shí)現(xiàn)代碼
}
}
注意
如果子類(lèi)繼承的構(gòu)造器能滿足必要構(gòu)造器的要求给郊,則無(wú)須在子類(lèi)中顯式提供必要構(gòu)造器的實(shí)現(xiàn)。
107.通過(guò)閉包或函數(shù)設(shè)置屬性的默認(rèn)值
如果某個(gè)存儲(chǔ)型屬性的默認(rèn)值需要一些定制或設(shè)置焕蹄,你可以使用閉包或全局函數(shù)為其提供定制的默認(rèn)值逾雄。每當(dāng)某個(gè)屬性所在類(lèi)型的新實(shí)例被創(chuàng)建時(shí),對(duì)應(yīng)的閉包或函數(shù)會(huì)被調(diào)用腻脏,而它們的返回值會(huì)當(dāng)做默認(rèn)值賦值給這個(gè)屬性鸦泳。
這種類(lèi)型的閉包或函數(shù)通常會(huì)創(chuàng)建一個(gè)跟屬性類(lèi)型相同的臨時(shí)變量,然后修改它的值以滿足預(yù)期的初始狀態(tài)永品,最后返回這個(gè)臨時(shí)變量做鹰,作為屬性的默認(rèn)值。
下面介紹了如何用閉包為屬性提供默認(rèn)值:
class SomeClass {
let someProperty: SomeType = {
//在這個(gè)閉包中給someProperty創(chuàng)建一個(gè)默認(rèn)值
// someValue必須和SomeType類(lèi)型相同
return someValue
}()
}
注意閉包結(jié)尾的大括號(hào)后面接了一對(duì)空的小括號(hào)鼎姐。這用來(lái)告訴Swift立即執(zhí)行此閉包钾麸。如果你忽略了這對(duì)括號(hào)更振,相當(dāng)于將閉包本身作為值賦值給了屬性,而不是將閉包的返回值賦值給屬性喂走。
注意
如果你使用閉包來(lái)初始化屬性殃饿,請(qǐng)記住在閉包執(zhí)行時(shí),實(shí)例的其它部分都還沒(méi)有初始化芋肠。這意味著你不能在閉包里訪問(wèn)其它屬性乎芳,即使這些屬性有默認(rèn)值。同樣帖池,你也不能使用隱式的self屬性奈惑,或者調(diào)用任何實(shí)例方法。
下面例子中定義了一個(gè)結(jié)構(gòu)體Checkerboard睡汹,它構(gòu)建了西洋跳棋游戲的棋盤(pán):
西洋跳棋游戲在一副黑白格交替的10x10的棋盤(pán)中進(jìn)行肴甸。為了呈現(xiàn)這副游戲棋盤(pán),Checkerboard結(jié)構(gòu)體定義了一個(gè)屬性boardColors囚巴,它是一個(gè)包含100個(gè)Bool值的數(shù)組原在。在數(shù)組中,值為true的元素表示一個(gè)黑格彤叉,值為false的元素表示一個(gè)白格庶柿。數(shù)組中第一個(gè)元素代表棋盤(pán)上左上角的格子,最后一個(gè)元素代表棋盤(pán)上右下角的格子秽浇。
boardColor數(shù)組是通過(guò)一個(gè)閉包來(lái)初始化并設(shè)置顏色值的:
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]
}
}
每當(dāng)一個(gè)新的Checkerboard實(shí)例被創(chuàng)建時(shí)浮庐,賦值閉包會(huì)被執(zhí)行,boardColors的默認(rèn)值會(huì)被計(jì)算出來(lái)并返回柬焕。上面例子中描述的閉包將計(jì)算出棋盤(pán)中每個(gè)格子對(duì)應(yīng)的顏色审残,并將這些值保存到一個(gè)臨時(shí)數(shù)組temporaryBoard中,最后在構(gòu)建完成時(shí)將此數(shù)組作為閉包返回值返回斑举。這個(gè)返回的數(shù)組會(huì)保存到boardColors中搅轿,并可以通過(guò)工具函數(shù)squareIsBlackAtRow來(lái)查詢:
let board = Checkerboard()
print(board.squareIsBlackAtRow(0, column: 1))
//打印"true"
print(board.squareIsBlackAtRow(9, column: 9))
//打印"false"
108.析構(gòu)過(guò)程原理
Swift會(huì)自動(dòng)釋放不再需要的實(shí)例以釋放資源。如自動(dòng)引用計(jì)數(shù)章節(jié)中所講述富玷,Swift通過(guò)自動(dòng)引用計(jì)數(shù)(ARC)處理實(shí)例的內(nèi)存管理介时。通常當(dāng)你的實(shí)例被釋放時(shí)不需要手動(dòng)地去清理。但是凌彬,當(dāng)使用自己的資源時(shí)沸柔,你可能需要進(jìn)行一些額外的清理。例如铲敛,如果創(chuàng)建了一個(gè)自定義的類(lèi)來(lái)打開(kāi)一個(gè)文件褐澎,并寫(xiě)入一些數(shù)據(jù),你可能需要在類(lèi)實(shí)例被釋放之前手動(dòng)去關(guān)閉該文件伐蒋。
在類(lèi)的定義中工三,每個(gè)類(lèi)最多只能有一個(gè)析構(gòu)器迁酸,而且析構(gòu)器不帶任何參數(shù),如下所示:
deinit {
//執(zhí)行析構(gòu)過(guò)程
}
析構(gòu)器是在實(shí)例釋放發(fā)生前被自動(dòng)調(diào)用俭正。你不能主動(dòng)調(diào)用析構(gòu)器奸鬓。子類(lèi)繼承了父類(lèi)的析構(gòu)器,并且在子類(lèi)析構(gòu)器實(shí)現(xiàn)的最后掸读,父類(lèi)的析構(gòu)器會(huì)被自動(dòng)調(diào)用串远。即使子類(lèi)沒(méi)有提供自己的析構(gòu)器,父類(lèi)的析構(gòu)器也同樣會(huì)被調(diào)用。
因?yàn)橹钡綄?shí)例的析構(gòu)器被調(diào)用后,實(shí)例才會(huì)被釋放玩郊,所以析構(gòu)器可以訪問(wèn)實(shí)例的所有屬性,并且可以根據(jù)那些屬性可以修改它的行為(比如查找一個(gè)需要被關(guān)閉的文件)留搔。
109.析構(gòu)器實(shí)踐
這是一個(gè)析構(gòu)器實(shí)踐的例子。這個(gè)例子描述了一個(gè)簡(jiǎn)單的游戲铛铁,這里定義了兩種新類(lèi)型隔显,分別是Bank和Player。Bank類(lèi)管理一種虛擬硬幣饵逐,確保流通的硬幣數(shù)量永遠(yuǎn)不可能超過(guò)10,000荣月。在游戲中有且只能有一個(gè)Bank存在,因此Bank用類(lèi)來(lái)實(shí)現(xiàn)梳毙,并使用靜態(tài)屬性和靜態(tài)方法來(lái)存儲(chǔ)和管理其當(dāng)前狀態(tài)。
class Bank {
static var coinsInBank = 10_000
static func vendCoins(var numberOfCoinsToVend: Int) -> Int {
numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receiveCoins(coins: Int) {
coinsInBank += coins
}
}
Bank使用coinsInBank屬性來(lái)跟蹤它當(dāng)前擁有的硬幣數(shù)量捐下。Bank還提供了兩個(gè)方法账锹,vendCoins(_:)和receiveCoins(_:),分別用來(lái)處理硬幣的分發(fā)和收集坷襟。
vendCoins(_:)方法在Bank對(duì)象分發(fā)硬幣之前檢查是否有足夠的硬幣奸柬。如果硬幣不足,Bank對(duì)象會(huì)返回一個(gè)比請(qǐng)求時(shí)小的數(shù)字(如果Bank對(duì)象中沒(méi)有硬幣了就返回0)婴程。vendCoins方法聲明numberOfCoinsToVend為一個(gè)變量參數(shù)廓奕,這樣就可以在方法體內(nèi)部修改分發(fā)的硬幣數(shù)量,而不需要定義一個(gè)新的變量档叔。vendCoins方法返回一個(gè)整型值桌粉,表示提供的硬幣的實(shí)際數(shù)量。
receiveCoins(_:)方法只是將Bank對(duì)象接收到的硬幣數(shù)目加回硬幣存儲(chǔ)中衙四。
Player類(lèi)描述了游戲中的一個(gè)玩家铃肯。每一個(gè)玩家在任意時(shí)間都有一定數(shù)量的硬幣存儲(chǔ)在他們的錢(qián)包中。這通過(guò)玩家的coinsInPurse屬性來(lái)表示:
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.vendCoins(coins)
}
func winCoins(coins: Int) {
coinsInPurse += Bank.vendCoins(coins)
}
deinit {
Bank.receiveCoins(coinsInPurse)
}
}
每個(gè)Player實(shí)例在初始化的過(guò)程中传蹈,都從Bank對(duì)象獲取指定數(shù)量的硬幣押逼。如果沒(méi)有足夠的硬幣可用步藕,Player實(shí)例可能會(huì)收到比指定數(shù)量少的硬幣.
Player類(lèi)定義了一個(gè)winCoins(_:)方法,該方法從Bank對(duì)象獲取一定數(shù)量的硬幣挑格,并把它們添加到玩家的錢(qián)包咙冗。Player類(lèi)還實(shí)現(xiàn)了一個(gè)析構(gòu)器,這個(gè)析構(gòu)器在Player實(shí)例釋放前被調(diào)用漂彤。在這里雾消,析構(gòu)器的作用只是將玩家的所有硬幣都返還給Bank對(duì)象:
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
//打印"A new player has joined the game with 100 coins"
print("There are now \(Bank.coinsInBank) coins left in the bank")
//打印"There are now 9900 coins left in the bank"
創(chuàng)建一個(gè)Player實(shí)例的時(shí)候,會(huì)向Bank對(duì)象請(qǐng)求100個(gè)硬幣显歧,如果有足夠的硬幣可用的話仪或。這個(gè)Player實(shí)例存儲(chǔ)在一個(gè)名為playerOne的可選類(lèi)型的變量中。這里使用了一個(gè)可選類(lèi)型的變量士骤,因?yàn)橥婕铱梢噪S時(shí)離開(kāi)游戲范删,設(shè)置為可選使你可以追蹤玩家當(dāng)前是否在游戲中。
因?yàn)閜layerOne是可選的拷肌,所以訪問(wèn)其coinsInPurse屬性來(lái)打印錢(qián)包中的硬幣數(shù)量時(shí)到旦,使用感嘆號(hào)(!)來(lái)解包:
playerOne!.winCoins(2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
//輸出"PlayerOne won 2000 coins & now has 2100 coins"
print("The bank now only has \(Bank.coinsInBank) coins left")
//輸出"The bank now only has 7900 coins left"
這里,玩家已經(jīng)贏得了2,000枚硬幣巨缘,所以玩家的錢(qián)包中現(xiàn)在有2,100枚硬幣添忘,而B(niǎo)ank對(duì)象只剩余7,900枚硬幣。
playerOne = nil
print("PlayerOne has left the game")
//打印"PlayerOne has left the game"
print("The bank now has \(Bank.coinsInBank) coins")
//打印"The bank now has 10000 coins"
玩家現(xiàn)在已經(jīng)離開(kāi)了游戲若锁。這通過(guò)將可選類(lèi)型的playerOne變量設(shè)置為nil來(lái)表示搁骑,意味著“沒(méi)有Player實(shí)例”。當(dāng)這一切發(fā)生時(shí)又固,playerOne變量對(duì)Player實(shí)例的引用被破壞了仲器。沒(méi)有其它屬性或者變量引用Player實(shí)例,因此該實(shí)例會(huì)被釋放仰冠,以便回收內(nèi)存乏冀。在這之前,該實(shí)例的析構(gòu)器被自動(dòng)調(diào)用洋只,玩家的硬幣被返還給銀行辆沦。
110.自動(dòng)引用計(jì)數(shù)實(shí)踐
下面的例子展示了自動(dòng)引用計(jì)數(shù)的工作機(jī)制。例子以一個(gè)簡(jiǎn)單的Person類(lèi)開(kāi)始识虚,并定義了一個(gè)叫name的常量屬性:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person類(lèi)有一個(gè)構(gòu)造函數(shù)肢扯,此構(gòu)造函數(shù)為實(shí)例的name屬性賦值,并打印一條消息以表明初始化過(guò)程生效担锤。Person類(lèi)也擁有一個(gè)析構(gòu)函數(shù)鹃彻,這個(gè)析構(gòu)函數(shù)會(huì)在實(shí)例被銷(xiāo)毀時(shí)打印一條消息。
接下來(lái)的代碼片段定義了三個(gè)類(lèi)型為Person?的變量妻献,用來(lái)按照代碼片段中的順序蛛株,為新的Person實(shí)例建立多個(gè)引用团赁。由于這些變量是被定義為可選類(lèi)型(Person?,而不是Person)谨履,它們的值會(huì)被自動(dòng)初始化為nil欢摄,目前還不會(huì)引用到Person類(lèi)的實(shí)例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
現(xiàn)在你可以創(chuàng)建Person類(lèi)的新實(shí)例笋粟,并且將它賦值給三個(gè)變量中的一個(gè):
reference1 = Person(name: "John Appleseed")
// prints "John Appleseed is being initialized”
應(yīng)當(dāng)注意到當(dāng)你調(diào)用Person類(lèi)的構(gòu)造函數(shù)的時(shí)候怀挠,“John Appleseed is being initialized”會(huì)被打印出來(lái)。由此可以確定構(gòu)造函數(shù)被執(zhí)行害捕。
由于Person類(lèi)的新實(shí)例被賦值給了reference1變量绿淋,所以reference1到Person類(lèi)的新實(shí)例之間建立了一個(gè)強(qiáng)引用。正是因?yàn)檫@一個(gè)強(qiáng)引用尝盼,ARC會(huì)保證Person實(shí)例被保持在內(nèi)存中不被銷(xiāo)毀吞滞。
如果你將同一個(gè)Person實(shí)例也賦值給其他兩個(gè)變量,該實(shí)例又會(huì)多出兩個(gè)強(qiáng)引用:
reference2 = reference1
reference3 = reference1
現(xiàn)在這一個(gè)Person實(shí)例已經(jīng)有三個(gè)強(qiáng)引用了盾沫。
如果你通過(guò)給其中兩個(gè)變量賦值nil的方式斷開(kāi)兩個(gè)強(qiáng)引用(包括最先的那個(gè)強(qiáng)引用)裁赠,只留下一個(gè)強(qiáng)引用,Person實(shí)例不會(huì)被銷(xiāo)毀:
reference1 = nil
reference2 = nil
在你清楚地表明不再使用這個(gè)Person實(shí)例時(shí)赴精,即第三個(gè)也就是最后一個(gè)強(qiáng)引用被斷開(kāi)時(shí)佩捞,ARC會(huì)銷(xiāo)毀它:
reference3 = nil
//打印“John Appleseed is being deinitialized”
111.類(lèi)實(shí)例之間的循環(huán)強(qiáng)引用
在上面的例子中,ARC會(huì)跟蹤你所新創(chuàng)建的Person實(shí)例的引用數(shù)量蕾哟,并且會(huì)在Person實(shí)例不再被需要時(shí)銷(xiāo)毀它一忱。
然而,我們可能會(huì)寫(xiě)出一個(gè)類(lèi)實(shí)例的強(qiáng)引用數(shù)永遠(yuǎn)不能變成0的代碼谭确。如果兩個(gè)類(lèi)實(shí)例互相持有對(duì)方的強(qiáng)引用,因而每個(gè)實(shí)例都讓對(duì)方一直存在琼富,就是這種情況。這就是所謂的循環(huán)強(qiáng)引用庄新。
你可以通過(guò)定義類(lèi)之間的關(guān)系為弱引用或無(wú)主引用鞠眉,以替代強(qiáng)引用,從而解決循環(huán)強(qiáng)引用的問(wèn)題择诈。具體的過(guò)程在解決類(lèi)實(shí)例之間的循環(huán)強(qiáng)引用中有描述械蹋。不管怎樣,在你學(xué)習(xí)怎樣解決循環(huán)強(qiáng)引用之前羞芍,很有必要了解一下它是怎樣產(chǎn)生的哗戈。
下面展示了一個(gè)不經(jīng)意產(chǎn)生循環(huán)強(qiáng)引用的例子。例子定義了兩個(gè)類(lèi):Person和Apartment荷科,用來(lái)建模公寓和它其中的居民:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每一個(gè)Person實(shí)例有一個(gè)類(lèi)型為String唯咬,名字為name的屬性纱注,并有一個(gè)可選的初始化為nil的apartment屬性。apartment屬性是可選的胆胰,因?yàn)橐粋€(gè)人并不總是擁有公寓狞贱。
類(lèi)似的,每個(gè)Apartment實(shí)例有一個(gè)叫unit蜀涨,類(lèi)型為String的屬性瞎嬉,并有一個(gè)可選的初始化為nil的tenant屬性。tenant屬性是可選的厚柳,因?yàn)橐粭澒⒉⒉豢偸怯芯用瘛?/p>
這兩個(gè)類(lèi)都定義了析構(gòu)函數(shù)氧枣,用以在類(lèi)實(shí)例被析構(gòu)的時(shí)候輸出信息。這讓你能夠知曉Person和Apartment的實(shí)例是否像預(yù)期的那樣被銷(xiāo)毀别垮。
接下來(lái)的代碼片段定義了兩個(gè)可選類(lèi)型的變量john和unit4A便监,并分別被設(shè)定為下面的Apartment和Person的實(shí)例。這兩個(gè)變量都被初始化為nil宰闰,這正是可選的優(yōu)點(diǎn):
var john: Person?
var unit4A: Apartment?
現(xiàn)在你可以創(chuàng)建特定的Person和Apartment實(shí)例并將賦值給john和unit4A變量:
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
在兩個(gè)實(shí)例被創(chuàng)建和賦值后茬贵,下圖表現(xiàn)了強(qiáng)引用的關(guān)系馋劈。變量john現(xiàn)在有一個(gè)指向Person實(shí)例的強(qiáng)引用乎莉,而變量unit4A有一個(gè)指向Apartment實(shí)例的強(qiáng)引用:
現(xiàn)在你能夠?qū)⑦@兩個(gè)實(shí)例關(guān)聯(lián)在一起咐鹤,這樣人就能有公寓住了誊涯,而公寓也有了房客簿寂。注意感嘆號(hào)是用來(lái)展開(kāi)和訪問(wèn)可選變量john和unit4A中的實(shí)例尘盼,這樣實(shí)例的屬性才能被賦值:
john!.apartment = unit4A
unit4A!.tenant = john
在將兩個(gè)實(shí)例聯(lián)系在一起之后谈飒,強(qiáng)引用的關(guān)系如圖所示:
不幸的是猾昆,這兩個(gè)實(shí)例關(guān)聯(lián)后會(huì)產(chǎn)生一個(gè)循環(huán)強(qiáng)引用觅够。Person實(shí)例現(xiàn)在有了一個(gè)指向Apartment實(shí)例的強(qiáng)引用,而Apartment實(shí)例也有了一個(gè)指向Person實(shí)例的強(qiáng)引用。因此派阱,當(dāng)你斷開(kāi)john和unit4A變量所持有的強(qiáng)引用時(shí)涡上,引用計(jì)數(shù)并不會(huì)降為0趾断,實(shí)例也不會(huì)被ARC銷(xiāo)毀:
john = nil
unit4A = nil
注意,當(dāng)你把這兩個(gè)變量設(shè)為nil時(shí)吩愧,沒(méi)有任何一個(gè)析構(gòu)函數(shù)被調(diào)用芋酌。循環(huán)強(qiáng)引用會(huì)一直阻止Person和Apartment類(lèi)實(shí)例的銷(xiāo)毀,這就在你的應(yīng)用程序中造成了內(nèi)存泄漏雁佳。
在你將john和unit4A賦值為nil后脐帝,強(qiáng)引用關(guān)系如下圖:
Person和Apartment實(shí)例之間的強(qiáng)引用關(guān)系保留了下來(lái)并且不會(huì)被斷開(kāi)。
解決實(shí)例之間的循環(huán)強(qiáng)引用
Swift提供了兩種辦法用來(lái)解決你在使用類(lèi)的屬性時(shí)所遇到的循環(huán)強(qiáng)引用問(wèn)題:弱引用(weak reference)和無(wú)主引用(unowned reference)糖权。
弱引用和無(wú)主引用允許循環(huán)引用中的一個(gè)實(shí)例引用另外一個(gè)實(shí)例而不保持強(qiáng)引用堵腹。這樣實(shí)例能夠互相引用而不產(chǎn)生循環(huán)強(qiáng)引用。
對(duì)于生命周期中會(huì)變?yōu)?/b>nil的實(shí)例使用弱引用星澳。相反地疚顷,對(duì)于初始化賦值后再也不會(huì)被賦值為nil的實(shí)例,使用無(wú)主引用禁偎。
112.弱引用
弱引用不會(huì)對(duì)其引用的實(shí)例保持強(qiáng)引用腿堤,因而不會(huì)阻止ARC銷(xiāo)毀被引用的實(shí)例。這個(gè)特性阻止了引用變?yōu)檠h(huán)強(qiáng)引用届垫。聲明屬性或者變量時(shí)释液,在前面加上weak關(guān)鍵字表明這是一個(gè)弱引用全释。
在實(shí)例的生命周期中装处,如果某些時(shí)候引用沒(méi)有值,那么弱引用可以避免循環(huán)強(qiáng)引用。如果引用總是有值妄迁,則可以使用無(wú)主引用寝蹈,在無(wú)主引用中有描述。在上面Apartment的例子中登淘,一個(gè)公寓的生命周期中箫老,有時(shí)是沒(méi)有“居民”的,因此適合使用弱引用來(lái)解決循環(huán)強(qiáng)引用黔州。
注意
弱引用必須被聲明為變量耍鬓,表明其值能在運(yùn)行時(shí)被修改。弱引用不能被聲明為常量流妻。
因?yàn)槿跻每梢詻](méi)有值牲蜀,你必須將每一個(gè)弱引用聲明為可選類(lèi)型。在Swift中绅这,推薦使用可選類(lèi)型描述可能沒(méi)有值的類(lèi)型涣达。
因?yàn)槿跻貌粫?huì)保持所引用的實(shí)例,即使引用存在证薇,實(shí)例也有可能被銷(xiāo)毀度苔。因此,ARC會(huì)在引用的實(shí)例被銷(xiāo)毀后自動(dòng)將其賦值為nil浑度。你可以像其他可選值一樣寇窑,檢查弱引用的值是否存在,你將永遠(yuǎn)不會(huì)訪問(wèn)已銷(xiāo)毀的實(shí)例的引用俺泣。
下面的例子跟上面Person和Apartment的例子一致疗认,但是有一個(gè)重要的區(qū)別。這一次伏钠,Apartment的tenant屬性被聲明為弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
然后跟之前一樣横漏,建立兩個(gè)變量(john和unit4A)之間的強(qiáng)引用,并關(guān)聯(lián)兩個(gè)實(shí)例:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
現(xiàn)在熟掂,兩個(gè)關(guān)聯(lián)在一起的實(shí)例的引用關(guān)系如下圖所示:
Person實(shí)例依然保持對(duì)Apartment實(shí)例的強(qiáng)引用缎浇,但是Apartment實(shí)例只持有對(duì)Person實(shí)例的弱引用。這意味著當(dāng)你斷開(kāi)john變量所保持的強(qiáng)引用時(shí)赴肚,再也沒(méi)有指向Person實(shí)例的強(qiáng)引用了:
由于再也沒(méi)有指向Person實(shí)例的強(qiáng)引用素跺,該實(shí)例會(huì)被銷(xiāo)毀:
john = nil
//打印“John Appleseed is being deinitialized”
唯一剩下的指向Apartment實(shí)例的強(qiáng)引用來(lái)自于變量unit4A。如果你斷開(kāi)這個(gè)強(qiáng)引用誉券,再也沒(méi)有指向Apartment實(shí)例的強(qiáng)引用了:
由于再也沒(méi)有指向Apartment實(shí)例的強(qiáng)引用指厌,該實(shí)例也會(huì)被銷(xiāo)毀:
unit4A = nil
//打印“Apartment 4A is being deinitialized”
上面的兩段代碼展示了變量john和unit4A在被賦值為nil后,Person實(shí)例和Apartment實(shí)例的析構(gòu)函數(shù)都打印出“銷(xiāo)毀”的信息踊跟。這證明了引用循環(huán)被打破了踩验。
注意
在使用垃圾收集的系統(tǒng)里,弱指針有時(shí)用來(lái)實(shí)現(xiàn)簡(jiǎn)單的緩沖機(jī)制,因?yàn)闆](méi)有強(qiáng)引用的對(duì)象只會(huì)在內(nèi)存壓力觸發(fā)垃圾收集時(shí)才被銷(xiāo)毀箕憾。但是在ARC中牡借,一旦值的最后一個(gè)強(qiáng)引用被移除,就會(huì)被立即銷(xiāo)毀袭异,這導(dǎo)致弱引用并不適合上面的用途钠龙。
113.無(wú)主引用
和弱引用類(lèi)似,無(wú)主引用不會(huì)牢牢保持住引用的實(shí)例御铃。和弱引用不同的是碴里,無(wú)主引用是永遠(yuǎn)有值的。因此上真,無(wú)主引用總是被定義為非可選類(lèi)型(non-optional type)并闲。你可以在聲明屬性或者變量時(shí),在前面加上關(guān)鍵字unowned表示這是一個(gè)無(wú)主引用谷羞。
由于無(wú)主引用是非可選類(lèi)型帝火,你不需要在使用它的時(shí)候?qū)⑺归_(kāi)。無(wú)主引用總是可以被直接訪問(wèn)湃缎。不過(guò)ARC無(wú)法在實(shí)例被銷(xiāo)毀后將無(wú)主引用設(shè)為nil犀填,因?yàn)榉强蛇x類(lèi)型的變量不允許被賦值為nil。
注意
如果你試圖在實(shí)例被銷(xiāo)毀后嗓违,訪問(wèn)該實(shí)例的無(wú)主引用九巡,會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤。使用無(wú)主引用蹂季,你必須確保引用始終指向一個(gè)未銷(xiāo)毀的實(shí)例冕广。
還需要注意的是如果你試圖訪問(wèn)實(shí)例已經(jīng)被銷(xiāo)毀的無(wú)主引用,Swift確保程序會(huì)直接崩潰偿洁,而不會(huì)發(fā)生無(wú)法預(yù)期的行為撒汉。所以你應(yīng)當(dāng)避免這樣的事情發(fā)生。
下面的例子定義了兩個(gè)類(lèi)涕滋,Customer和CreditCard睬辐,模擬了銀行客戶和客戶的信用卡。這兩個(gè)類(lèi)中宾肺,每一個(gè)都將另外一個(gè)類(lèi)的實(shí)例作為自身的屬性溯饵。這種關(guān)系可能會(huì)造成循環(huán)強(qiáng)引用。
Customer和CreditCard之間的關(guān)系與前面弱引用例子中Apartment和Person的關(guān)系略微不同锨用。在這個(gè)數(shù)據(jù)模型中丰刊,一個(gè)客戶可能有或者沒(méi)有信用卡,但是一張信用卡總是關(guān)聯(lián)著一個(gè)客戶增拥。為了表示這種關(guān)系啄巧,Customer類(lèi)有一個(gè)可選類(lèi)型的card屬性洪橘,但是CreditCard類(lèi)有一個(gè)非可選類(lèi)型的customer屬性。
此外棵帽,只能通過(guò)將一個(gè)number值和customer實(shí)例傳遞給CreditCard構(gòu)造函數(shù)的方式來(lái)創(chuàng)建CreditCard實(shí)例。這樣可以確保當(dāng)創(chuàng)建CreditCard實(shí)例時(shí)總是有一個(gè)customer實(shí)例與之關(guān)聯(lián)渣玲。
由于信用卡總是關(guān)聯(lián)著一個(gè)客戶逗概,因此將customer屬性定義為無(wú)主引用,用以避免循環(huán)強(qiáng)引用:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
注意
CreditCard類(lèi)的number屬性被定義為UInt64類(lèi)型而不是Int類(lèi)型忘衍,以確保number屬性的存儲(chǔ)量在32位和64位系統(tǒng)上都能足夠容納16位的卡號(hào)逾苫。
下面的代碼片段定義了一個(gè)叫john的可選類(lèi)型Customer變量,用來(lái)保存某個(gè)特定客戶的引用枚钓。由于是可選類(lèi)型铅搓,所以變量被初始化為nil:
var john: Customer?
現(xiàn)在你可以創(chuàng)建Customer類(lèi)的實(shí)例,用它初始化CreditCard實(shí)例搀捷,并將新創(chuàng)建的CreditCard實(shí)例賦值為客戶的card屬性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
在你關(guān)聯(lián)兩個(gè)實(shí)例后,它們的引用關(guān)系如下圖所示:
Customer實(shí)例持有對(duì)CreditCard實(shí)例的強(qiáng)引用,而CreditCard實(shí)例持有對(duì)Customer實(shí)例的無(wú)主引用确徙。
由于customer的無(wú)主引用叛复,當(dāng)你斷開(kāi)john變量持有的強(qiáng)引用時(shí),再也沒(méi)有指向Customer實(shí)例的強(qiáng)引用了:
由于再也沒(méi)有指向Customer實(shí)例的強(qiáng)引用家厌,該實(shí)例被銷(xiāo)毀了播玖。其后,再也沒(méi)有指向CreditCard實(shí)例的強(qiáng)引用饭于,該實(shí)例也隨之被銷(xiāo)毀了:
john = nil
//打印“John Appleseed is being deinitialized”
//打印”Card #1234567890123456 is being deinitialized”
最后的代碼展示了在john變量被設(shè)為nil后Customer實(shí)例和CreditCard實(shí)例的構(gòu)造函數(shù)都打印出了“銷(xiāo)毀”的信息蜀踏。
114.無(wú)主引用以及隱式解析可選屬性
上面弱引用和無(wú)主引用的例子涵蓋了兩種常用的需要打破循環(huán)強(qiáng)引用的場(chǎng)景。
Person和Apartment的例子展示了兩個(gè)屬性的值都允許為nil掰吕,并會(huì)潛在的產(chǎn)生循環(huán)強(qiáng)引用果覆。這種場(chǎng)景最適合用弱引用來(lái)解決。
Customer和CreditCard的例子展示了一個(gè)屬性的值允許為nil殖熟,而另一個(gè)屬性的值不允許為nil随静,這也可能會(huì)產(chǎn)生循環(huán)強(qiáng)引用。這種場(chǎng)景最適合通過(guò)無(wú)主引用來(lái)解決吗讶。
然而燎猛,存在著第三種場(chǎng)景,在這種場(chǎng)景中照皆,兩個(gè)屬性都必須有值重绷,并且初始化完成后永遠(yuǎn)不會(huì)為nil。在這種場(chǎng)景中膜毁,需要一個(gè)類(lèi)使用無(wú)主屬性昭卓,而另外一個(gè)類(lèi)使用隱式解析可選屬性愤钾。
這使兩個(gè)屬性在初始化完成后能被直接訪問(wèn)(不需要可選展開(kāi)),同時(shí)避免了循環(huán)引用候醒。這一節(jié)將為你展示如何建立這種關(guān)系能颁。
下面的例子定義了兩個(gè)類(lèi),Country和City倒淫,每個(gè)類(lèi)將另外一個(gè)類(lèi)的實(shí)例保存為屬性伙菊。在這個(gè)模型中,每個(gè)國(guó)家必須有首都敌土,每個(gè)城市必須屬于一個(gè)國(guó)家镜硕。為了實(shí)現(xiàn)這種關(guān)系,Country類(lèi)擁有一個(gè)capitalCity屬性返干,而City類(lèi)有一個(gè)country屬性:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
為了建立兩個(gè)類(lèi)的依賴關(guān)系兴枯,City的構(gòu)造函數(shù)接受一個(gè)Country實(shí)例作為參數(shù),并且將實(shí)例保存到country屬性矩欠。
Country的構(gòu)造函數(shù)調(diào)用了City的構(gòu)造函數(shù)财剖。然而,只有Country的實(shí)例完全初始化后癌淮,Country的構(gòu)造函數(shù)才能把self傳給City的構(gòu)造函數(shù)峰伙。(在兩段式構(gòu)造過(guò)程中有具體描述)
為了滿足這種需求,通過(guò)在類(lèi)型結(jié)尾處加上感嘆號(hào)(City!)的方式该默,將Country的capitalCity屬性聲明為隱式解析可選類(lèi)型的屬性瞳氓。這意味著像其他可選類(lèi)型一樣,capitalCity屬性的默認(rèn)值為nil栓袖,但是不需要展開(kāi)它的值就能訪問(wèn)它匣摘。(在隱式解析可選類(lèi)型中有描述)
由于capitalCity默認(rèn)值為nil,一旦Country的實(shí)例在構(gòu)造函數(shù)中給name屬性賦值后裹刮,整個(gè)初始化過(guò)程就完成了音榜。這意味著一旦name屬性被賦值后,Country的構(gòu)造函數(shù)就能引用并傳遞隱式的self捧弃。Country的構(gòu)造函數(shù)在賦值capitalCity時(shí)赠叼,就能將self作為參數(shù)傳遞給City的構(gòu)造函數(shù)。
以上的意義在于你可以通過(guò)一條語(yǔ)句同時(shí)創(chuàng)建Country和City的實(shí)例违霞,而不產(chǎn)生循環(huán)強(qiáng)引用嘴办,并且capitalCity的屬性能被直接訪問(wèn),而不需要通過(guò)感嘆號(hào)來(lái)展開(kāi)它的可選值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
//打印“Canada's capital city is called Ottawa”
在上面的例子中买鸽,使用隱式解析可選值意味著滿足了類(lèi)的構(gòu)造函數(shù)的兩個(gè)構(gòu)造階段的要求涧郊。capitalCity屬性在初始化完成后,能像非可選值一樣使用和存取眼五,同時(shí)還避免了循環(huán)強(qiáng)引用妆艘。
115.閉包引起的循環(huán)強(qiáng)引用
前面我們看到了循環(huán)強(qiáng)引用是在兩個(gè)類(lèi)實(shí)例屬性互相保持對(duì)方的強(qiáng)引用時(shí)產(chǎn)生的彤灶,還知道了如何用弱引用和無(wú)主引用來(lái)打破這些循環(huán)強(qiáng)引用。
循環(huán)強(qiáng)引用還會(huì)發(fā)生在當(dāng)你將一個(gè)閉包賦值給類(lèi)實(shí)例的某個(gè)屬性批旺,并且這個(gè)閉包體中又使用了這個(gè)類(lèi)實(shí)例時(shí)幌陕。這個(gè)閉包體中可能訪問(wèn)了實(shí)例的某個(gè)屬性,例如self.someProperty汽煮,或者閉包中調(diào)用了實(shí)例的某個(gè)方法搏熄,例如self.someMethod()。這兩種情況都導(dǎo)致了閉包“捕獲”self逗物,從而產(chǎn)生了循環(huán)強(qiáng)引用。
循環(huán)強(qiáng)引用的產(chǎn)生瑟俭,是因?yàn)殚]包和類(lèi)相似翎卓,都是引用類(lèi)型。當(dāng)你把一個(gè)閉包賦值給某個(gè)屬性時(shí)摆寄,你是將這個(gè)閉包的引用賦值給了屬性失暴。實(shí)質(zhì)上,這跟之前的問(wèn)題是一樣的——兩個(gè)強(qiáng)引用讓彼此一直有效微饥。但是逗扒,和兩個(gè)類(lèi)實(shí)例不同,這次一個(gè)是類(lèi)實(shí)例欠橘,另一個(gè)是閉包矩肩。
Swift提供了一種優(yōu)雅的方法來(lái)解決這個(gè)問(wèn)題,稱(chēng)之為閉包捕獲列表(closure capture list)肃续。同樣的黍檩,在學(xué)習(xí)如何用閉包捕獲列表打破循環(huán)強(qiáng)引用之前,先來(lái)了解一下這里的循環(huán)強(qiáng)引用是如何產(chǎn)生的始锚,這對(duì)我們很有幫助刽酱。
下面的例子為你展示了當(dāng)一個(gè)閉包引用了self后是如何產(chǎn)生一個(gè)循環(huán)強(qiáng)引用的。例子中定義了一個(gè)叫HTMLElement的類(lèi)瞧捌,用一種簡(jiǎn)單的模型表示HTML文檔中的一個(gè)單獨(dú)的元素:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
if let text = self.text {
return "<\(self.name)>\(text)"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElement類(lèi)定義了一個(gè)name屬性來(lái)表示這個(gè)元素的名稱(chēng)棵里,例如代表段落的“p”,或者代表?yè)Q行的“br”姐呐。HTMLElement還定義了一個(gè)可選屬性text殿怜,用來(lái)設(shè)置HTML元素呈現(xiàn)的文本。
除了上面的兩個(gè)屬性曙砂,HTMLElement還定義了一個(gè)lazy屬性asHTML稳捆。這個(gè)屬性引用了一個(gè)將name和text組合成HTML字符串片段的閉包。該屬性是Void -> String類(lèi)型麦轰,或者可以理解為“一個(gè)沒(méi)有參數(shù)乔夯,返回String的函數(shù)”砖织。
默認(rèn)情況下,閉包賦值給了asHTML屬性末荐,這個(gè)閉包返回一個(gè)代表HTML標(biāo)簽的字符串侧纯。如果text值存在,該標(biāo)簽就包含可選值text甲脏;如果text不存在眶熬,該標(biāo)簽就不包含文本。對(duì)于段落元素块请,根據(jù)text是“some text”還是nil娜氏,閉包會(huì)返回"
some text
"或者""。
可以像實(shí)例方法那樣去命名墩新、使用asHTML屬性贸弥。然而,由于asHTML是閉包而不是實(shí)例方法海渊,如果你想改變特定HTML元素的處理方式的話绵疲,可以用自定義的閉包來(lái)取代默認(rèn)值。
例如臣疑,可以將一個(gè)閉包賦值給asHTML屬性盔憨,這個(gè)閉包能在text屬性是nil時(shí)使用默認(rèn)文本,這是為了避免返回一個(gè)空的HTML標(biāo)簽:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)"
}
print(heading.asHTML())
//打印“
some default text
”
注意
asHTML聲明為lazy屬性讯沈,因?yàn)橹挥挟?dāng)元素確實(shí)需要被處理為HTML輸出的字符串時(shí)郁岩,才需要使用asHTML。也就是說(shuō)缺狠,在默認(rèn)的閉包中可以使用self驯用,因?yàn)橹挥挟?dāng)初始化完成以及self確實(shí)存在后,才能訪問(wèn)lazy屬性儒老。
HTMLElement類(lèi)只提供了一個(gè)構(gòu)造函數(shù)蝴乔,通過(guò)name和text(如果有的話)參數(shù)來(lái)初始化一個(gè)新元素。該類(lèi)也定義了一個(gè)析構(gòu)函數(shù)驮樊,當(dāng)HTMLElement實(shí)例被銷(xiāo)毀時(shí)薇正,打印一條消息。
下面的代碼展示了如何用HTMLElement類(lèi)創(chuàng)建實(shí)例并打印消息:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
//打印“
hello, world
”
注意
上面的paragraph變量定義為可選類(lèi)型的HTMLElement囚衔,因此我們可以賦值nil給它來(lái)演示循環(huán)強(qiáng)引用挖腰。
不幸的是,上面寫(xiě)的HTMLElement類(lèi)產(chǎn)生了類(lèi)實(shí)例和作為asHTML默認(rèn)值的閉包之間的循環(huán)強(qiáng)引用。循環(huán)強(qiáng)引用如下圖所示:
實(shí)例的asHTML屬性持有閉包的強(qiáng)引用。但是村砂,閉包在其閉包體內(nèi)使用了self(引用了self.name和self.text)欧宜,因此閉包捕獲了self辽俗,這意味著閉包又反過(guò)來(lái)持有了HTMLElement實(shí)例的強(qiáng)引用疾渣。這樣兩個(gè)對(duì)象就產(chǎn)生了循環(huán)強(qiáng)引用。(更多關(guān)于閉包捕獲值的信息崖飘,請(qǐng)參考值捕獲)榴捡。
注意
雖然閉包多次使用了self,它只捕獲HTMLElement實(shí)例的一個(gè)強(qiáng)引用朱浴。
如果設(shè)置paragraph變量為nil吊圾,打破它持有的HTMLElement實(shí)例的強(qiáng)引用,HTMLElement實(shí)例和它的閉包都不會(huì)被銷(xiāo)毀翰蠢,也是因?yàn)檠h(huán)強(qiáng)引用:
paragraph = nil
注意项乒,HTMLElement的析構(gòu)函數(shù)中的消息并沒(méi)有被打印,證明了HTMLElement實(shí)例并沒(méi)有被銷(xiāo)毀梁沧。
解決閉包引起的循環(huán)強(qiáng)引用
在定義閉包時(shí)同時(shí)定義捕獲列表作為閉包的一部分檀何,通過(guò)這種方式可以解決閉包和類(lèi)實(shí)例之間的循環(huán)強(qiáng)引用。捕獲列表定義了閉包體內(nèi)捕獲一個(gè)或者多個(gè)引用類(lèi)型的規(guī)則趁尼。跟解決兩個(gè)類(lèi)實(shí)例間的循環(huán)強(qiáng)引用一樣埃碱,聲明每個(gè)捕獲的引用為弱引用或無(wú)主引用猖辫,而不是強(qiáng)引用酥泞。應(yīng)當(dāng)根據(jù)代碼關(guān)系來(lái)決定使用弱引用還是無(wú)主引用。
注意
Swift有如下要求:只要在閉包內(nèi)使用self的成員啃憎,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())芝囤。這提醒你可能會(huì)一不小心就捕獲了self。
定義捕獲列表
捕獲列表中的每一項(xiàng)都由一對(duì)元素組成辛萍,一個(gè)元素是weak或unowned關(guān)鍵字悯姊,另一個(gè)元素是類(lèi)實(shí)例的引用(例如self)或初始化過(guò)的變量(如delegate = self.delegate!)。這些項(xiàng)在方括號(hào)中用逗號(hào)分開(kāi)贩毕。
如果閉包有參數(shù)列表和返回類(lèi)型悯许,把捕獲列表放在它們前面:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
//這里是閉包的函數(shù)體
}
如果閉包沒(méi)有指明參數(shù)列表或者返回類(lèi)型,即它們會(huì)通過(guò)上下文推斷辉阶,那么可以把捕獲列表和關(guān)鍵字in放在閉包最開(kāi)始的地方:
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
//這里是閉包的函數(shù)體
}
弱引用和無(wú)主引用
在閉包和捕獲的實(shí)例總是互相引用并且總是同時(shí)銷(xiāo)毀時(shí)先壕,將閉包內(nèi)的捕獲定義為無(wú)主引用。
相反的谆甜,在被捕獲的引用可能會(huì)變?yōu)閚il時(shí)垃僚,將閉包內(nèi)的捕獲定義為弱引用。弱引用總是可選類(lèi)型规辱,并且當(dāng)引用的實(shí)例被銷(xiāo)毀后谆棺,弱引用的值會(huì)自動(dòng)置為nil。這使我們可以在閉包體內(nèi)檢查它們是否存在罕袋。
注意
如果被捕獲的引用絕對(duì)不會(huì)變?yōu)閚il改淑,應(yīng)該用無(wú)主引用碍岔,而不是弱引用。
前面的HTMLElement例子中溅固,無(wú)主引用是正確的解決循環(huán)強(qiáng)引用的方法付秕。這樣編寫(xiě)HTMLElement類(lèi)來(lái)避免循環(huán)強(qiáng)引用:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
上面的HTMLElement實(shí)現(xiàn)和之前的實(shí)現(xiàn)一致,除了在asHTML閉包中多了一個(gè)捕獲列表侍郭。這里询吴,捕獲列表是[unowned self],表示“將self捕獲為無(wú)主引用而不是強(qiáng)引用”亮元。
和之前一樣猛计,我們可以創(chuàng)建并打印HTMLElement實(shí)例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
//打印“
hello, world
”
使用捕獲列表后引用關(guān)系如下圖所示:
這一次,閉包以無(wú)主引用的形式捕獲self爆捞,并不會(huì)持有HTMLElement實(shí)例的強(qiáng)引用奉瘤。如果將paragraph賦值為nil,HTMLElement實(shí)例將會(huì)被銷(xiāo)毀煮甥,并能看到它的析構(gòu)函數(shù)打印出的消息:
paragraph = nil
//打印“p is being deinitialized”
你可以查看捕獲列表章節(jié)盗温,獲取更多關(guān)于捕獲列表的信息。
116.使用可選鏈?zhǔn)秸{(diào)用代替強(qiáng)制展開(kāi)
通過(guò)在想調(diào)用的屬性成肘、方法卖局、或下標(biāo)的可選值(optional value)后面放一個(gè)問(wèn)號(hào)(?),可以定義一個(gè)可選鏈双霍。這一點(diǎn)很像在可選值后面放一個(gè)嘆號(hào)(!)來(lái)強(qiáng)制展開(kāi)它的值砚偶。它們的主要區(qū)別在于當(dāng)可選值為空時(shí)可選鏈?zhǔn)秸{(diào)用只會(huì)調(diào)用失敗,然而強(qiáng)制展開(kāi)將會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤洒闸。
為了反映可選鏈?zhǔn)秸{(diào)用可以在空值(nil)上調(diào)用的事實(shí)染坯,不論這個(gè)調(diào)用的屬性、方法及下標(biāo)返回的值是不是可選值丘逸,它的返回結(jié)果都是一個(gè)可選值单鹿。你可以利用這個(gè)返回值來(lái)判斷你的可選鏈?zhǔn)秸{(diào)用是否調(diào)用成功,如果調(diào)用有返回值則說(shuō)明調(diào)用成功深纲,返回nil則說(shuō)明調(diào)用失敗仲锄。
特別地,可選鏈?zhǔn)秸{(diào)用的返回結(jié)果與原本的返回結(jié)果具有相同的類(lèi)型囤萤,但是被包裝成了一個(gè)可選值昼窗。例如,使用可選鏈?zhǔn)秸{(diào)用訪問(wèn)屬性涛舍,當(dāng)可選鏈?zhǔn)秸{(diào)用成功時(shí)澄惊,如果屬性原本的返回結(jié)果是Int類(lèi)型,則會(huì)變?yōu)镮nt?類(lèi)型。
下面幾段代碼將解釋可選鏈?zhǔn)秸{(diào)用和強(qiáng)制展開(kāi)的不同掸驱。
首先定義兩個(gè)類(lèi)Person和Residence:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence有一個(gè)Int類(lèi)型的屬性numberOfRooms肛搬,其默認(rèn)值為1。Person具有一個(gè)可選的residence屬性毕贼,其類(lèi)型為Residence?温赔。
如果創(chuàng)建一個(gè)新的Person實(shí)例,因?yàn)樗膔esidence屬性是可選的鬼癣,john屬性將初始化為nil:
let john = Person()
如果使用嘆號(hào)(!)強(qiáng)制展開(kāi)獲得這個(gè)john的residence屬性中的numberOfRooms值陶贼,會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤,因?yàn)檫@時(shí)residence沒(méi)有可以展開(kāi)的值:
let roomCount = john.residence!.numberOfRooms
//這會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤
john.residence為非nil值的時(shí)候待秃,上面的調(diào)用會(huì)成功拜秧,并且把roomCount設(shè)置為Int類(lèi)型的房間數(shù)量。正如上面提到的章郁,當(dāng)residence為nil的時(shí)候上面這段代碼會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤枉氮。
可選鏈?zhǔn)秸{(diào)用提供了另一種訪問(wèn)numberOfRooms的方式,使用問(wèn)號(hào)(?)來(lái)替代原來(lái)的嘆號(hào)(!):
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
//打印“Unable to retrieve the number of rooms.”
在residence后面添加問(wèn)號(hào)之后暖庄,Swift就會(huì)在residence不為nil的情況下訪問(wèn)numberOfRooms聊替。
因?yàn)樵L問(wèn)numberOfRooms有可能失敗,可選鏈?zhǔn)秸{(diào)用會(huì)返回Int?類(lèi)型培廓,或稱(chēng)為“可選的Int”惹悄。如上例所示,當(dāng)residence為nil的時(shí)候医舆,可選的Int將會(huì)為nil俘侠,表明無(wú)法訪問(wèn)numberOfRooms象缀。訪問(wèn)成功時(shí)蔬将,可選的Int值會(huì)通過(guò)可選綁定展開(kāi),并賦值給非可選類(lèi)型的roomCount常量央星。
要注意的是霞怀,即使numberOfRooms是非可選的Int時(shí),這一點(diǎn)也成立莉给。只要使用可選鏈?zhǔn)秸{(diào)用就意味著numberOfRooms會(huì)返回一個(gè)Int?而不是Int毙石。
可以將一個(gè)Residence的實(shí)例賦給john.residence,這樣它就不再是nil了:
john.residence = Residence()
john.residence現(xiàn)在包含一個(gè)實(shí)際的Residence實(shí)例颓遏,而不再是nil徐矩。如果你試圖使用先前的可選鏈?zhǔn)秸{(diào)用訪問(wèn)numberOfRooms,它現(xiàn)在將返回值為1的Int?類(lèi)型的值:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
//打印“John's residence has 1 room(s).”
117.為可選鏈?zhǔn)秸{(diào)用定義模型類(lèi)
通過(guò)使用可選鏈?zhǔn)秸{(diào)用可以調(diào)用多層屬性叁幢、方法和下標(biāo)滤灯。這樣可以在復(fù)雜的模型中向下訪問(wèn)各種子屬性,并且判斷能否訪問(wèn)子屬性的屬性、方法或下標(biāo)鳞骤。
下面這段代碼定義了四個(gè)模型類(lèi)窒百,這些例子包括多層可選鏈?zhǔn)秸{(diào)用。為了方便說(shuō)明豫尽,在Person和Residence的基礎(chǔ)上增加了Room類(lèi)和Address類(lèi)篙梢,以及相關(guān)的屬性、方法以及下標(biāo)美旧。
Person類(lèi)的定義基本保持不變:
class Person {
var residence: Residence?
}
Residence類(lèi)比之前復(fù)雜些渤滞,增加了一個(gè)名為rooms的變量屬性,該屬性被初始化為[Room]類(lèi)型的空數(shù)組:
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
現(xiàn)在Residence有了一個(gè)存儲(chǔ)Room實(shí)例的數(shù)組榴嗅,numberOfRooms屬性被實(shí)現(xiàn)為計(jì)算型屬性蔼水,而不是存儲(chǔ)型屬性。numberOfRooms屬性簡(jiǎn)單地返回rooms數(shù)組的count屬性的值录肯。
Residence還提供了訪問(wèn)rooms數(shù)組的快捷方式趴腋,即提供可讀寫(xiě)的下標(biāo)來(lái)訪問(wèn)rooms數(shù)組中指定位置的元素。
此外论咏,Residence還提供了printNumberOfRooms()方法优炬,這個(gè)方法的作用是打印numberOfRooms的值。
最后厅贪,Residence還定義了一個(gè)可選屬性address蠢护,其類(lèi)型為Address?。Address類(lèi)的定義在下面會(huì)說(shuō)明养涮。
Room類(lèi)是一個(gè)簡(jiǎn)單類(lèi)蛔趴,其實(shí)例被存儲(chǔ)在rooms數(shù)組中航揉。該類(lèi)只包含一個(gè)屬性name,以及一個(gè)用于將該屬性設(shè)置為適當(dāng)?shù)姆块g名的初始化函數(shù):
class Room {
let name: String
init(name: String) { self.name = name }
}
最后一個(gè)類(lèi)是Address,這個(gè)類(lèi)有三個(gè)String?類(lèi)型的可選屬性洁奈。buildingName以及buildingNumber屬性分別表示某個(gè)大廈的名稱(chēng)和號(hào)碼棒旗,第三個(gè)屬性street表示大廈所在街道的名稱(chēng):
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName != nil {
return buildingName
} else if buildingNumber != nil && street != nil {
return "\(buildingNumber) \(street)"
} else {
return nil
}
}
}
Address類(lèi)提供了buildingIdentifier()方法熟菲,返回值為String?矢空。如果buildingName有值則返回buildingName∨澜ⅲ或者们陆,如果buildingNumber和street均有值則返回buildingNumber。否則情屹,返回nil坪仇。
通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)屬性
正如使用可選鏈?zhǔn)秸{(diào)用代替強(qiáng)制展開(kāi)中所述,可以通過(guò)可選鏈?zhǔn)秸{(diào)用在一個(gè)可選值上訪問(wèn)它的屬性垃你,并判斷訪問(wèn)是否成功椅文。
下面的代碼創(chuàng)建了一個(gè)Person實(shí)例颈墅,然后像之前一樣,嘗試訪問(wèn)numberOfRooms屬性:
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
//打印“Unable to retrieve the number of rooms.”
因?yàn)閖ohn.residence為nil雾袱,所以這個(gè)可選鏈?zhǔn)秸{(diào)用依舊會(huì)像先前一樣失敗恤筛。
還可以通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)設(shè)置屬性值:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
在這個(gè)例子中,通過(guò)john.residence來(lái)設(shè)定address屬性也會(huì)失敗芹橡,因?yàn)閖ohn.residence當(dāng)前為nil毒坛。
上面代碼中的賦值過(guò)程是可選鏈?zhǔn)秸{(diào)用的一部分,這意味著可選鏈?zhǔn)秸{(diào)用失敗時(shí)林说,等號(hào)右側(cè)的代碼不會(huì)被執(zhí)行煎殷。對(duì)于上面的代碼來(lái)說(shuō),很難驗(yàn)證這一點(diǎn)腿箩,因?yàn)橄襁@樣賦值一個(gè)常量沒(méi)有任何副作用豪直。下面的代碼完成了同樣的事情,但是它使用一個(gè)函數(shù)來(lái)創(chuàng)建Address實(shí)例珠移,然后將該實(shí)例返回用于賦值弓乙。該函數(shù)會(huì)在返回前打印“Function was called”,這使你能驗(yàn)證等號(hào)右側(cè)的代碼是否被執(zhí)行钧惧。
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
沒(méi)有任何打印消息暇韧,可以看出createAddress()函數(shù)并未被執(zhí)行。
通過(guò)可選鏈?zhǔn)秸{(diào)用調(diào)用方法
可以通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用方法浓瞪,并判斷是否調(diào)用成功懈玻,即使這個(gè)方法沒(méi)有返回值。
Residence類(lèi)中的printNumberOfRooms()方法打印當(dāng)前的numberOfRooms值乾颁,如下所示:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
這個(gè)方法沒(méi)有返回值涂乌。然而,沒(méi)有返回值的方法具有隱式的返回類(lèi)型Void英岭,如無(wú)返回值函數(shù)中所述湾盒。這意味著沒(méi)有返回值的方法也會(huì)返回(),或者說(shuō)空的元組巴席。
如果在可選值上通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用這個(gè)方法历涝,該方法的返回類(lèi)型會(huì)是Void?诅需,而不是Void漾唉,因?yàn)橥ㄟ^(guò)可選鏈?zhǔn)秸{(diào)用得到的返回值都是可選的。這樣我們就可以使用if語(yǔ)句來(lái)判斷能否成功調(diào)用printNumberOfRooms()方法堰塌,即使方法本身沒(méi)有定義返回值赵刑。通過(guò)判斷返回值是否為nil可以判斷調(diào)用是否成功:
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
//打印“It was not possible to print the number of rooms.”
同樣的,可以據(jù)此判斷通過(guò)可選鏈?zhǔn)秸{(diào)用為屬性賦值是否成功场刑。在上面的通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)屬性的例子中般此,我們嘗試給john.residence中的address屬性賦值蚪战,即使residence為nil。通過(guò)可選鏈?zhǔn)秸{(diào)用給屬性賦值會(huì)返回Void?铐懊,通過(guò)判斷返回值是否為nil就可以知道賦值是否成功:
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
//打印“It was not possible to set the address.”
通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)下標(biāo)
通過(guò)可選鏈?zhǔn)秸{(diào)用邀桑,我們可以在一個(gè)可選值上訪問(wèn)下標(biāo),并且判斷下標(biāo)調(diào)用是否成功科乎。
注意
通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)可選值的下標(biāo)時(shí)壁畸,應(yīng)該將問(wèn)號(hào)放在下標(biāo)方括號(hào)的前面而不是后面∶┟可選鏈?zhǔn)秸{(diào)用的問(wèn)號(hào)一般直接跟在可選表達(dá)式的后面捏萍。
下面這個(gè)例子用下標(biāo)訪問(wèn)john.residence屬性存儲(chǔ)的Residence實(shí)例的rooms數(shù)組中的第一個(gè)房間的名稱(chēng),因?yàn)閖ohn.residence為nil空闲,所以下標(biāo)調(diào)用失敗了:
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
//打印“Unable to retrieve the first room name.”
在這個(gè)例子中令杈,問(wèn)號(hào)直接放在john.residence的后面,并且在方括號(hào)的前面碴倾,因?yàn)閖ohn.residence是可選值逗噩。
類(lèi)似的,可以通過(guò)下標(biāo)跌榔,用可選鏈?zhǔn)秸{(diào)用來(lái)賦值:
john.residence?[0] = Room(name: "Bathroom")
這次賦值同樣會(huì)失敗给赞,因?yàn)閞esidence目前是nil。
如果你創(chuàng)建一個(gè)Residence實(shí)例矫户,并為其rooms數(shù)組添加一些Room實(shí)例片迅,然后將Residence實(shí)例賦值給john.residence,那就可以通過(guò)可選鏈和下標(biāo)來(lái)訪問(wèn)數(shù)組中的元素:
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
//打印“The first room name is Living Room.”
訪問(wèn)可選類(lèi)型的下標(biāo)
如果下標(biāo)返回可選類(lèi)型值皆辽,比如Swift中Dictionary類(lèi)型的鍵的下標(biāo)柑蛇,可以在下標(biāo)的結(jié)尾括號(hào)后面放一個(gè)問(wèn)號(hào)來(lái)在其可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0]++
testScores["Brian"]?[0] = 72
// "Dave"數(shù)組現(xiàn)在是[91, 82, 84],"Bev"數(shù)組現(xiàn)在是[80, 94, 81]
上面的例子中定義了一個(gè)testScores數(shù)組驱闷,包含了兩個(gè)鍵值對(duì)耻台,把String類(lèi)型的鍵映射到一個(gè)Int值的數(shù)組。這個(gè)例子用可選鏈?zhǔn)秸{(diào)用把"Dave"數(shù)組中第一個(gè)元素設(shè)為91空另,把"Bev"數(shù)組的第一個(gè)元素+1盆耽,然后嘗試把"Brian"數(shù)組中的第一個(gè)元素設(shè)為72。前兩個(gè)調(diào)用成功扼菠,因?yàn)閠estScores字典中包含"Dave"和"Bev"這兩個(gè)鍵摄杂。但是testScores字典中沒(méi)有"Brian"這個(gè)鍵,所以第三個(gè)調(diào)用失敗循榆。
連接多層可選鏈?zhǔn)秸{(diào)用
可以通過(guò)連接多個(gè)可選鏈?zhǔn)秸{(diào)用在更深的模型層級(jí)中訪問(wèn)屬性析恢、方法以及下標(biāo)。然而秧饮,多層可選鏈?zhǔn)秸{(diào)用不會(huì)增加返回值的可選層級(jí)映挂。
也就是說(shuō):
如果你訪問(wèn)的值不是可選的泽篮,可選鏈?zhǔn)秸{(diào)用將會(huì)返回可選值。
如果你訪問(wèn)的值就是可選的柑船,可選鏈?zhǔn)秸{(diào)用不會(huì)讓可選返回值變得“更可選”帽撑。
因此:
通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)一個(gè)Int值,將會(huì)返回Int?鞍时,無(wú)論使用了多少層可選鏈?zhǔn)秸{(diào)用油狂。
類(lèi)似的,通過(guò)可選鏈?zhǔn)秸{(diào)用訪問(wèn)Int?值寸癌,依舊會(huì)返回Int?值专筷,并不會(huì)返回Int??。
下面的例子嘗試訪問(wèn)john中的residence屬性中的address屬性中的street屬性蒸苇。這里使用了兩層可選鏈?zhǔn)秸{(diào)用磷蛹,residence以及address都是可選值:
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
//打印“Unable to retrieve the address.”
john.residence現(xiàn)在包含一個(gè)有效的Residence實(shí)例。然而溪烤,john.residence.address的值當(dāng)前為nil味咳。因此,調(diào)用john.residence?.address?.street會(huì)失敗檬嘀。
需要注意的是槽驶,上面的例子中,street的屬性為String?鸳兽。john.residence?.address?.street的返回值也依然是String?掂铐,即使已經(jīng)使用了兩層可選鏈?zhǔn)秸{(diào)用。
如果為john.residence.address賦值一個(gè)Address實(shí)例揍异,并且為address中的street屬性設(shè)置一個(gè)有效值全陨,我們就能過(guò)通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)訪問(wèn)street屬性:
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
//打印“John's street name is Laurel Street.”
在上面的例子中,因?yàn)閖ohn.residence包含一個(gè)有效的Residence實(shí)例衷掷,所以對(duì)john.residence的address屬性賦值將會(huì)成功辱姨。
在方法的可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用
上面的例子展示了如何在一個(gè)可選值上通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)獲取它的屬性值。我們還可以在一個(gè)可選值上通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用方法戚嗅,并且可以根據(jù)需要繼續(xù)在方法的可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用雨涛。
在下面的例子中,通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用Address的buildingIdentifier()方法懦胞。這個(gè)方法返回String?類(lèi)型的值替久。如上所述,通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用該方法医瘫,最終的返回值依舊會(huì)是String?類(lèi)型:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
//打印“John's building identifier is The Larches.”
如果要在該方法的返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用侣肄,在方法的圓括號(hào)后面加上問(wèn)號(hào)即可:
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
//打印“John's building identifier begins with "The".”
注意
在上面的例子中,在方法的圓括號(hào)后面加上問(wèn)號(hào)是因?yàn)槟阋?/b>buildingIdentifier()方法的可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用醇份,而不是方法本身稼锅。