1.屬性字符串
let string = "Test Attributed Strings"
let attributedString = NSMutableAttributedString(string: string)
let firstAttributes: [String : Any] = [NSForegroundColorAttributeName: UIColor.blue, NSBackgroundColorAttributeName: UIColor.yellow, NSUnderlineStyleAttributeName: 1]
attributedString.addAttributes(firstAttributes, range: NSRange(location: 0, length: 3))
2.Optional 實現(xiàn)
enum OptionalValue<T> {
case none
case some(T)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
3.可選解析
你可以包含多個可選綁定或多個布爾條件在一個 if 語句中,只要使用逗號分開就行。只要有任意一個可選綁定 的值為nil,或者任意一個布爾條件為false,則整個if條件判斷為false,這時你就需要使用嵌套 if 條 件語句來處理,如下所示:
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
// Prints "4 < 42 < 100"
if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
}
}
4.隱式解析可選類型
當(dāng)可選類型被第一次賦值之后就可以確定之后一直有值的時候,隱式解析可選類型非常有用。
一個隱式解析可選類型其實就是一個普通的可選類型,但是可以被當(dāng)做非可選類型來使用,并不需要每次都使用 解析來獲取可選值。下面的例子展示了可選類型 String 和隱式解析可選類型 String 之間的區(qū)別
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 需要感嘆號來獲取值
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 不需要感嘆號
注意:
如果你在隱式解析可選類型沒有值的時候嘗試取值,會觸發(fā)運行時錯誤。和你在沒有值的普通可選類型后面加一個驚嘆號一樣。
你仍然可以把隱式解析可選類型當(dāng)做普通可選類型來判斷它是否包含值:
if assumedString != nil {
print(assumedString)
}
// 輸出 "An implicitly unwrapped optional string."
//你也可以在可選綁定中使用隱式解析可選類型來檢查并解析它的值:
if let definiteString = assumedString {
print(definiteString)
}
// 輸出 "An implicitly unwrapped optional string."
注意:
如果一個變量之后可能變成 nil 的話請不要使用隱式解析可選類型。如果你需要在變量的生命周期中判斷是否是nil的話,請使用普通可選類型。
5.錯誤處理
相對于可選中運用值的存在與缺失來表達函數(shù)的成功與失敗,錯誤處理可以推斷失敗的原因,并傳播至程序的其他部分。
當(dāng)一個函數(shù)遇到錯誤條件,它會拋出一個錯誤。調(diào)用函數(shù)的調(diào)用者能捕獲該錯誤并進行相應(yīng)處理。
一個函數(shù)可以通過在聲明中添加 throws 關(guān)鍵字來拋出錯誤消息。
當(dāng)你調(diào)用的函數(shù)能拋出錯誤時,應(yīng)該在表達式中前置 try 關(guān)鍵字
func canThrowAnError() throws { // 這個函數(shù)有可能拋出錯誤
}
do {
try canThrowAnError() // 沒有錯誤消息拋出
} catch {
// 有一個錯誤消息拋出
}
6.空和運算符號 ??
空合運算符 ( a ?? b ) 將對 可選類型 a 進行空判斷,如果 a 包含一個值就進行解封,否則就返回一個默認
值 b 。表達式 a 必須是 Optional 類型。默認值 b 的類型必須要和 a 存儲值的類型保持一致。
空合運算符是對以下代碼的簡短表達方法:
a != nil ? a! : b
注意: 如果 a 為非空值( non-nil ),那么值 b 將不會被計算。這也就是所謂的短路求值
7.字符串
Swift 的 String 是值類型 如果您創(chuàng)建了一個新的字符串,那么當(dāng)其進行常量、變量賦值操作,或在函數(shù)/ 方法中傳遞時,會進行值拷貝。 任何情況下,都會對已有字符串值創(chuàng)建新副本,并對該新副本進行傳遞或賦值操作。
Swift 默認字符串拷貝的方式保證了在函數(shù)/方法中傳遞的是字符串的值。 很明顯無論該值來自于哪里,都是您獨自擁有的。 您可以確保傳遞的字符串不會被修改,除非你自己去修改它。
在實際編譯時,Swift 編譯器會優(yōu)化字符串的使用,使實際的復(fù)制只發(fā)生在絕對必要的情況下 ,這意味著在將字符串作為值類型的同時可以獲得極高的性能。
遍歷字符串:
您可通過 for-in 循環(huán)來遍歷字符串中的 characters 屬性 來獲取每一個字符的值:
//Swif3 中通過 `.characters`屬性來遍歷
for character in "Dog!??". characters {
print(character)
}
// D
// o
// g
// !
// ??
//Swift4 中直接通過 string 進行遍歷
for character in "Dog!??" {
print(character)
}
// D
// o
// g
// !
// ??
計算字符數(shù)量:
// Swift3
let unusualMenagerie = "Koala ??, Snail ??, Penguin ??, Dromedary ??" print("unusualMenagerie has \(unusualMenagerie.characters.count) characters") // 打印輸出 "unusualMenagerie has 40 characters"
//Swift4
let unusualMenagerie = "Koala ??, Snail ??, Penguin ??, Dromedary ??"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// Prints "unusualMenagerie has 40 characters"
使用 characters 屬性的 indices 屬性會創(chuàng)建一個包含全部索引的范圍(Range),用來在一個字符串中訪問單 個字符。
let greeting = "Guten Tag!"
// Swift3
for index in greeting.characters.indices {
print("\(greeting[index]) ", terminator: "")
}
// 打印輸出 "G u t e n T a g ! "
// Swift4
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n T a g ! "
8.多行字符串 Swift4
如果需要跨多行的字符串,請使用多行字符串文字。多行字符串文字是由三個雙引號包圍的字符序列:
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
因為多行格式使用三個雙引號而不是僅一個,所以可以在多行字符串文字中包含一個雙引號( "),如上面的示例所示。要 在多行字符串中包含 """ 文字,必須使用反斜杠( \ )來轉(zhuǎn)義至少一個引號。例如:
let threeDoubleQuotes = """
Escaping the first quote \"""
Escaping all three quotes \"\"\"
"""
在其多行字符串中,字符串文字包括其開頭和結(jié)尾引號之間的所有行。該字符串在開頭的引號( """ )之后的第一行開始,并在結(jié)束引號( """ )之前的行結(jié)束,這意味著quotation不會以換行符開頭或結(jié)尾。以下兩個字符串相同:
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
要使用換行符開頭或結(jié)尾的多行字符串文字,請將空行寫為第一行或最后一行。例如:
"""
This string starts with a line feed.
It also ends with a line feed.
"""
9. 遍歷數(shù)組
如果我們同時需要每個數(shù)據(jù)項的值和索引值,可以使用 enumerated() 方法來進行數(shù)組遍歷。 enumerated() 返回 一個由每一個數(shù)據(jù)項索引值和數(shù)據(jù)值組成的元組。我們可以把這個元組分解成臨時常量或者變量來進行遍歷:
var list = ["Six eggs", "Milk", "Flour", "Baking Powder", "Bananas"]
for item in list {
print(item)
}
// 同時遍歷每個數(shù)據(jù)項的索引和值
for (index, value) in list.enumerated() {
print("Item \(index + 1): \(value)")
}
10.集合
合類型的哈希值
一個類型為了存儲在集合中,該類型必須是 可哈希化 的--也就是說,該類型必須提供一個方法來計算它的哈希值。一個哈希值是 Int 類型的,相等的對象哈希值必須相同,比如 a==b ,因此必須 a.hashValue == b.hashValue 。
Swift 的所有基本類型(比如 String , Int , Double 和 Bool )默認都是可哈?;?,可以作為集合的值的類型或者字典的鍵的類型。沒有 關(guān)聯(lián)值 的枚舉成員值默認也是可哈希化的。
注意: 你可以使用你自定義的類型作為集合的值的類型或者是字典的鍵的類型,但你需要使你的自定義類型符合 Swift 標準庫中的
Hashable協(xié)議。符合Hashable協(xié)議的類型需要提供一個類型為Int的可讀屬性hashValue。由類型的hashValue屬性返回的值不需要在同一程序的不同執(zhí)行周期或者不同程序之間保持相同。
因為Hashable協(xié)議符合Equatable協(xié)議,所以遵循該協(xié)議的類型也必須提供一個"是否相等"運算符(==)的實 現(xiàn)。這個 Equatable 協(xié)議要求任何符合==實現(xiàn)的實例間都是一種相等的關(guān)系。也就是說,對于 a,b,c 三個值來 說, == 的實現(xiàn)必須滿足下面三種情況:
?a == a(自反性)
?a == b意味著b == a(對稱性)
?a == b && b == c意味著a == c(傳遞性)
e.g:
沒有遵循 Hashable 的 AClass 無法放到集合中
class AClass {
var name: String = "a"
}
let c1 = AClass()
let c2 = AClass()
var sets: Set = [c1, c2]
// error cannot convert value of type '[AClass]' to specified type 'Set'
遵循了 Hashable 的 BClass 可放到集合中
class BClass {
var name: String = "a"
}
extension BClass: Hashable {
var hashValue: Int {
return name.hashValue
}
static func ==(lhs: BClass, rhs: BClass) -> Bool {
return lhs.name == rhs.name
}
}
let b1 = BClass()
let b2 = BClass()
var sets: Set = [b1, b2]
11.Switch
不存在隱式的貫穿
與 C 和 Objective-C 中的 switch 語句不同,在 Swift 中,當(dāng)匹配的 case 分支中的代碼執(zhí)行完畢后,程序會終止 switch 語句,而不會繼續(xù)執(zhí)行下一個 case 分支。這也就是說,不需要在 case 分支中顯式地使用 break 語 句。這使得 switch 語句更安全、更易用,也避免了因忘記寫 break 語句而產(chǎn)生的錯誤。
每一個 case 分支都必須至少包含一條語句,下面這樣的代碼是錯誤的,因為第一個 case 分支是空的
let anotherCharacter: Character = "a" switch anotherCharacter {
case "a": // 無效,這個分支下面沒有語句 case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 這段代碼會報編譯錯誤
為了讓單個 case 同時匹配 a 和 A ,可以將這兩個值組合成一個 復(fù)合匹配 ,并用逗號隔開:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 輸出 "The letter A
區(qū)間匹配
case 分支的模式也可以是一個值的區(qū)間。下面的例子展示了如何使用區(qū)間匹配來輸出任意數(shù)字對應(yīng)的自然語言格式:
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
var naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).") // 輸出 "There are dozens of moons orbiting Saturn."
元組
我們可以使用元組在同一個 switch 語句中測試多個值。元組中的元素可以是值,也可以是區(qū)間。另外,使用下劃 線( _ )來匹配所有可能的值。
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box”
值綁定
case 分支允許將匹配的值綁定到一個臨時的常量或變量,并且在case分支體內(nèi)使用 —— 這種行為被稱為值綁定(value binding),因為匹配的值在case分支體內(nèi),與臨時的常量或變量綁定
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 輸出 "on the x-axis with an x value of 2"
Where
case 分支的模式可以使用 where 語句來判斷額外的條件
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 輸出 "(1, -1) is on the line x == -y"
復(fù)合匹配
當(dāng)多個條件可以使用同一種方法來處理時,可以將這幾種可能放在同一個 case 后面,并且用逗號隔開。當(dāng) case 后面的任意一種模式匹配的時候,這條分支就會被匹配。并且,如果匹配列表過長,還可以分行書寫:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 輸出 "e is a vowel"
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 輸出 "On an axis, 9 from the origin"
12. fallthrough 貫穿
fallthrough 關(guān)鍵字不會檢查它下一個將會落入執(zhí)行的 case 中的匹配條件。 fallthrough 簡單地使代 碼繼續(xù)連接到下一個 case 中的代碼,這和 C 語言標準中的 switch 語句特性是一樣的。
let character = "a"
switch character {
case "a":
print("One")
fallthrough
case "b":
print("Two ")
default:
print("default")
}
//One
//Two
13. 帶標簽的語句
在 Swift 中,你可以在循環(huán)體和條件語句中嵌套循環(huán)體和條件語句來創(chuàng)造復(fù)雜的控制流結(jié)構(gòu)。并且,循環(huán)體和條件語句都可以使用 break 語句來提前結(jié)束整個代碼塊。因此,顯式地指明 break 語句想要終止的是哪個循環(huán)體或者條件語句會很有用。類似地,如果你有許多嵌套的循環(huán)體,顯式指明 continue 語句想要影響哪一個循環(huán)體也會非常有用。
為了實現(xiàn)這個目的,你可以使用標簽 (statement label) 來標記一個循環(huán)體或者條件語句,對于一個條件語 句,你可以使用 break 加標簽的方式來結(jié)束這個被標記的語句。對于一個循環(huán)語句,你可以使用 break 或者 continue 加標簽,來結(jié)束或者繼續(xù)這條被標記語句的執(zhí)行。
聲明一個帶標簽的語句是通過在該語句的關(guān)鍵詞的同一行前面放置一個標簽,作為這個語句的前導(dǎo)關(guān)鍵字 (introd ucor keyword) ,并且該標簽后面跟隨一個冒號。下面是一個針對 while 循環(huán)體的標簽語法,同樣的規(guī)則適用于所有的循環(huán)體和條件語句。
label name: whilecondition{statements}
let loop1 = 0...5
let loop2 = 0...5
loop1: for l1 in loop1 {
print("loop1")
loop2: for l2 in loop2 {
print("loop2")
if l2 == 3 {
break loop1 //顯式的結(jié)束loop1循環(huán)
}
}
}
/* 輸出
loop1
loop2
loop2
loop2
loop2
*/
14. 枚舉
關(guān)聯(lián)值
你可以定義 Swift 枚舉來存儲任意類型的關(guān)聯(lián)值,如果需要的話,每個枚舉成員的關(guān)聯(lián)值類型可以各不相同。枚舉的這種特性跟其他語言中的 可識別聯(lián)合(discriminated unions) ,標簽聯(lián)合(tagged unions) ,或者 變體(variants) 相似。
e.g.
定義一個名為 Barcode 的枚舉類型,它的一個成員值是具有 (Int,Int,Int,Int) 類型關(guān)聯(lián)值的 upc ,另一個 成員值是具有 String 類型關(guān)聯(lián)值的 qrCode 。
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
switch 中提取關(guān)聯(lián)值:
你可以在 switch 的 case 分支代碼中提取每個關(guān)聯(lián)值作為一個常量(用 let 前綴)或者 作為一個變量(用 var 前綴)來使用:
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
// 打印 "QR code: ABCDEFGHIJKLMNOP."
如果一個枚舉成員的所有關(guān)聯(lián)值都被提取為常量,或者都被提取為變量,為了簡潔,你可以只在成員名稱前標注一個 let 或者 var:
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// 輸出 "QR code: ABCDEFGHIJKLMNOP."
原始值
作為關(guān)聯(lián)值的替代選擇,枚舉成員可以被默認值(稱為原始值 raw values )預(yù)填充,這些原始值的類型必須相同。
Note: 要設(shè)置原始值,枚舉必須繼承自一個類型:
原始值可以是字符串,字符,或者任意整型值或浮點型值。每個原始值在枚舉聲明中必須是唯一的。
// 這樣會編譯錯誤
enum ASCIIControlCharacter {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
// 需要繼承自一個類型,如 Character 才可以設(shè)置原始值
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
注意
原始值和關(guān)聯(lián)值是不同的。原始值是在定義枚舉時被預(yù)先填充的值,像上述三個 ASCII 碼。對于一個特定的枚舉成員,它的原始值始終不變。關(guān)聯(lián)值是創(chuàng)建一個基于枚舉成員的常量或變量時才設(shè)置的值,枚舉成員的關(guān)聯(lián)值可以變化。
原始值的隱式賦值
在使用原始值為整數(shù)或者字符串類型的枚舉時,不需要顯式地為每一個枚舉成員設(shè)置原始值,Swift 將會自動為你賦值。
例如,當(dāng)使用整數(shù)作為原始值時,隱式賦值的值依次遞增 1 。如果第一個枚舉成員沒有設(shè)置原始值,其原始值將為0。
enum Planet: Int {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let p = Planet.mercury
p.rawValue
// p.rawValue = 0
當(dāng)使用字符串作為枚舉類型的原始值時,每個枚舉成員的隱式原始值為該枚舉成員的名稱。
enum CompassPoint: String {
case north, south, east, west
}
let earthsOrder = Planet.earth.rawValue // earthsOrder 值為 3
let sunsetDirection = CompassPoint.west.rawValue // sunsetDirection 值為 "west"
使用原始值初始化枚舉實例
如果在定義枚舉類型的時候使用了原始值,那么將會自動獲得一個初始化方法,這個方法接收一個叫做 rawValue 的參數(shù),參數(shù)類型即為原始值類型,返回值則是枚舉成員或 nil 。你可以使用這個初始化方法來創(chuàng)建一個新的枚舉實例。
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 類型為 Planet? 值為 Planet.uranus
然而,并非所有 Int 值都可以找到一個匹配的行星。因此,原始值構(gòu)造器總是返回一個 可選 的枚舉成員。在上面的例子中, possiblePlanet 是 Planet? 類型,或者說“可選的 Planet ”。
注意
原始值構(gòu)造器是一個可失敗構(gòu)造器,因為并不是每一個原始值都有與之對應(yīng)的枚舉成員。
遞歸枚舉
遞歸枚舉是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關(guān)聯(lián)值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上 indirect 來表示該成員可遞歸。
例如,下面的例子中,枚舉類型存儲了簡單的算術(shù)表達式:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
你也可以在枚舉類型開頭加上 indirect 關(guān)鍵字來表明它的所有成員都是可遞歸的:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
/**
上面定義的枚舉類型可以存儲三種算術(shù)表達式:純數(shù)字、兩個表達式相加、兩個表達式相乘。
枚舉成員 addition 和 multiplication 的關(guān)聯(lián)值也是算術(shù)表達式——這些關(guān)聯(lián)值使得嵌套表達式成為可能。
例如,表達式 (5 + 4) * 2 ,乘號右邊是一個數(shù)字,左邊則是另一個表達式。
因為數(shù)據(jù)是嵌套的,因而用來存儲數(shù)據(jù)的枚舉類型也需要支持這種嵌套——這意味著枚舉類型需要支持遞歸。
下面的代碼展示了使用 ArithmeticExpression 這個遞歸枚舉創(chuàng) 建表達式 (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))
// 要操作具有遞歸性質(zhì)的數(shù)據(jù)結(jié)構(gòu),使用遞歸函數(shù)是一種直截了當(dāng)?shù)姆绞健@?,下面是一個對算術(shù)表達式求值的函數(shù):
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product)) // 打印 "18"
15. 屬性
延遲存儲屬性
延遲存儲屬性是指當(dāng)?shù)谝淮伪徽{(diào)用的時候才會計算其初始值的屬性。在屬性聲明前使用 lazy 來標示一個延遲存儲屬性。
注意
必須將延遲存儲屬性聲明成變量(使用var關(guān)鍵字),因為屬性的初始值可能在實例構(gòu)造完成之后才會得到。而常量屬性在構(gòu)造過程完成之前必須要有初始值,因此無法聲明成延遲屬性。
延遲屬性很有用,當(dāng)屬性的值依賴于在實例的構(gòu)造過程結(jié)束后才會知道影響值的外部因素時,或者當(dāng)獲得屬性的初始值需要復(fù)雜或大量計算時,可以只在需要的時候計算它。
class DataImporter {
/*
DataImporter 是一個負責(zé)將外部文件中的數(shù)據(jù)導(dǎo)入的類。 這個類的初始化會消耗不少時間。
*/
var fileName = "data.txt"
// 這里會提供數(shù)據(jù)導(dǎo)入功能 }
class DataManager {
lazy var importer = DataImporter() var data = [String]()
// 這里會提供數(shù)據(jù)管理功能
}
let manager = DataManager() manager.data.append("Some data") manager.data.append("Some more data")
// DataImporter 實例的 importer 屬性還沒有被創(chuàng)建
print(manager.importer.fileName)
// DataImporter 實例的 importer 屬性現(xiàn)在被創(chuàng)建了 // 輸出 "data.txt”
注意
如果一個被標記為lazy的屬性在沒有初始化時就同時被多個線程訪問,則無法保證該屬性只會被初始化一 次。
存儲屬性和實例變量 Stored Properties and Instance Variable
Swift 中沒有實例變量這個概念
Objective-C 為類實例存儲值和引用提供兩種方法。除了 屬性 之外,還可以使用 實例變量 作為屬性值的后端存儲。
Swift 編程語言中把這些理論統(tǒng)一用屬性來實現(xiàn)。Swift 中的屬性沒有對應(yīng)的實例變量,屬性的后端存儲也無法直接訪問。這就避免了不同場景下訪問方式的困擾,同時也將屬性的定義簡化成一個語句。屬性的全部信息——包括命名、類型和內(nèi)存管理特征——都在唯一一個地方(類型定義中)定義。
計算屬性
除存儲屬性外,類、結(jié)構(gòu)體和枚舉可以定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可 選的 setter,來間接獲取和設(shè)置其他屬性或變量的值。
struct SomeStruct {
var width = 0.0, height = 0.0
var length: Double {
get {
return width + height
}
set(newLength) {
width = newLength * 0.5
height = newLength * 0.5
}
}
}
簡化 setter 聲明
如果計算屬性的 setter 沒有定義表示新值的參數(shù)名,則可以使用默認名稱 newValue 。下面是使用了簡化 setter 聲明的結(jié)構(gòu)體代碼:
struct SomeStruct {
var width = 0.0, height = 0.0
var length: Double {
get {
return width + height
}
set {
width = newValue * 0.5
height = newValue * 0.5
}
}
}
只讀計算屬性
只有 getter 沒有 setter 的計算屬性就是只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設(shè)置新的值。
只讀計算屬性的聲明可以去掉 get 關(guān)鍵字和花括號:
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"
屬性觀察器
屬性觀察器 監(jiān)控和響應(yīng)屬性值的變化,每次屬性被設(shè)置值的時候都會調(diào)用屬性觀察器,即使新值和當(dāng)前值相同的時候也不例外。
可以為除了 延遲存儲屬性 之外的其他存儲屬性添加屬性觀察器,也可以通過重寫屬性的方式為繼承的屬性(包括存儲屬性和計算屬性)添加屬性觀察器。你不必為非重寫的計算屬性添加屬性觀察器,因為可以通過它的 setter 直接監(jiān)控和響應(yīng)值的變化。
-
willSet在新的值被設(shè)置之前調(diào)用 -
didSet在新的值被設(shè)置之后立即調(diào)用
willSet 觀察器將 新的屬性值 作為常量參數(shù)傳入,可以為該參數(shù)指定一個名稱,如果不指定參數(shù)名,則使用默認參數(shù)名 newValue 表示
didSet 觀察器將 舊的屬性值 作為參數(shù)傳入,也可以為該參數(shù)指定一個參數(shù)名,不指定則使用默認參數(shù)名 oldValue 表示。 如果 在 didSet 方法中再次對該屬性賦值,那么新值會覆蓋舊的值。
注意:
父類的屬性在子類的構(gòu)造器中被賦值時,它在父類中的觀察器會被調(diào)用,隨后才會調(diào)用子類的觀察器。在父類初始化方法調(diào)用之前,子類給屬性賦值時,觀察器不會被調(diào)用。
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("Did set totalSteps: \(totalSteps) oldValue:\(oldValue)")
} else {
// 可以再次對totalSteps 進行賦值,并且不會觸發(fā)屬性觀察器
totalSteps = 0
}
}
}
}
16.在實例方法中修改值類型
結(jié)構(gòu)體和枚舉是 值類型 ,默認情況下,值類型的屬性不能再它的實例方法中被修改。
但是,如果確實需要在某個特定的方法中修改結(jié)構(gòu)體或者枚舉的屬性,可以為這個方法選擇 可變(mytating) 行為,然后就可以在該方法內(nèi)部改變它的屬性。并且這個方法的任何改變都會在方法執(zhí)行結(jié)束時寫回到原始的結(jié)構(gòu)體中。 方法還可以給它隱含的 self 屬性賦值一個全新的實例,這個新實例在方法結(jié)束時會替換現(xiàn)存實例。
要使用 可變 方法,將關(guān)鍵字 mutating 放到方法的 func 關(guān)鍵字之前就可以了:
也可以在可變方法中給 self 賦值,可變方法能夠賦給隱含屬性 self 一個全新的實例。
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
mutating func reChangeSelf() {
self = Point(x: 0.0, y: 0.0)
}
}
var p = Point(x: 1.0, y: 1.0)
p.moveBy(deltaX: 2.0, y: 3.0)
p.reChangeSelf()
17.類型方法
定義在類型本身上調(diào)用的方法叫做 類型方法 。
在方法的 func 關(guān)鍵字之前加上關(guān)鍵字 static 來指定類型方法。類還要用關(guān)鍵字 class 來允許自雷重寫父類的方法實現(xiàn)。
在 Swift 中,可以為所有的 類、結(jié)構(gòu)體和枚舉定義類型方法。每一個類型方法都能被他所支持的類型顯式調(diào)用。
class SomeClass {
// class 修飾的類型方法可在子類中重寫
class func someTypeMethod() {
print("Called someTypeMethod")
}
// static 修飾的類型方法不可在子類中重寫
static func someTypeMethod2() {
print("Called someTypeMethod2")
}
}
SomeClass.someTypeMethod()
SomeClass.someTypeMethod2()
class SomeChildClass :SomeClass {
override class func someTypeMethod() {
print("Override Child someTypeMethod")
}
}
18.下標
下標可以定義在類、結(jié)構(gòu)體和枚舉中,是訪問集合,列表或序列中元素的快捷方式??梢允褂孟聵说乃饕O(shè)置和獲取值,而不需要再調(diào)用對應(yīng)的存取方法。如 通過下標訪問一個 Array 實例中的元素可以寫作 someArray[index] , 訪問 Dictionary 中的實例元素可以寫作 someDictionary[key] 。
一個類型可以定義多個下標,通過不同索引類型進行重載。 下標不限于一維,可以定義具有多個參數(shù)的下標滿足自定義類型的需求。
下標語法
下標允許通過在實例名稱后面的方括號中傳入一個或者多個索引值來對實例進行存取。
下標語法
與實例方法類似,定義下標使用 subscript 關(guān)鍵字,指定一個或多個輸入?yún)?shù)和返回類型;下標可以設(shè)定為讀寫或只讀, 這種行為由 getter 和 setter 實現(xiàn),類似于計算屬性。
subscript(index: Int) -> Int {
get {
// return an appropriate subscript value here
}
set(newValue) {
// perform a suitable setting action here
}
}
newValue 的類型和下標返回的類型相同。如同計算屬性,可以不指定 setter 的參數(shù)( newValue), 如果不指定參數(shù), setter 提供一個名為 newValue 的默認參數(shù).
如同只讀計算屬性,可以省略只讀下標的 get 關(guān)鍵字:
subscript(index: Int) -> Int {
// 返回一個適當(dāng)?shù)?Int 類型的值
}
下面代碼演示了只讀下標的實現(xiàn),這里定義了一個 TimesTable 結(jié)構(gòu)體,用來表示傳入整數(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
19.繼承
基類: 不繼承于其他的類,稱為基類。
Swift 中的類并不是從一個通用的基類繼承而來。如果不為定義的類指定一個超類的話,這個類就自動成為基類
訪問超類的方法、屬性及下標
在合適的地方,可以通過使用 super 前綴來訪問超類版本的方法,屬性或下標:
- 在方法
someMethod()的重寫實現(xiàn)中,可以通過super.someMethod()來調(diào)用超類版本的someMethod()方法 - 在屬性
someProperty的getter或setter的重寫實現(xiàn)中,可以通過super.someProperty來訪問超類版本的someProperty屬性 - 在下標的重寫中,可以通過
super[someIndex]來訪問超類版本的相同下標
重寫屬性
可以重寫屬性的 實例屬性 或 類型屬性 ,提供自己定制的 getter 和 setter ,或添加屬性觀察器使重寫的屬性可以觀察屬性值什么時候發(fā)生改變。
重寫屬性的 Getters 和 Setters
可以提供定制的 getter 或 setter 來重寫任意繼承來的屬性,無論繼承來的屬性是存儲屬性還是計算型屬性。 子類并不知道繼承來的屬性是存儲型還是計算型的,它只知道繼承來的屬性會有一個名字和類型。 在重寫一個屬性時,必須將它的名稱和類型都寫出來,這樣才能是編譯器去檢查你重寫的屬性是與超類中同名同類型的屬性相匹配的。
可以將一個繼承來的只讀屬性重寫為可讀寫屬性。只需要在子類中提供 setter 和 getter 的實現(xiàn)即可。但是,你不能將一個繼承來的讀寫屬性重寫為一個只讀屬性。
class SomeClass {
var name : String {
get {
return "SomeClass"
}
}
}
class SomeSubClass : SomeClass {
/// 可以將繼承來的只讀屬性重寫為一個讀寫屬性
override var name: String {
get {
return name
}
set {
name = newValue
}
}
}
當(dāng)嘗試將繼承來的讀寫屬性重寫為只讀屬性時,編譯器會報錯
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
/// 不能將繼承來的讀寫屬性重寫為一個只讀屬性
//error: cannot override mutable property with read-only property 'name'
override var name: String {
get {
return name
}
}
}
注意:
如果你在重寫屬性中提供了setter,那么你也一定要提供getter。如果你不想在重寫版本中的getter里修改繼承來的屬性值,你可以直接通過super.someProperty來返回繼承來的值,其中someProperty是你要重寫的屬性的名字。
重寫屬性觀察器
可以通過重寫屬性為一個繼承來的屬性添加屬性觀察器。這樣一來,當(dāng)繼承來的屬性值發(fā)生改變時,就會被通知到。
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
didSet {
print("Did Set")
}
willSet {
print("Will Set")
}
}
}
注意
你不可以為繼承來的常量存儲型屬性或繼承來的只讀計算型屬性添加屬性觀察器。這些屬性的值是不可以被設(shè)置的,所以,為它們提供willSet或didSet實現(xiàn)是不恰當(dāng)?shù)摹?br> 此外還要注意,你不可以同時提供重寫的setter和重寫的屬性觀察器。如果你想觀察屬性值的變化,并且你已經(jīng)為那個屬性提供了定制的setter,那么你在setter中就可以觀察到任何值變化了。
- 子類中只重寫了
getter也不能同時重寫屬性觀察器
編譯報錯:error: willSet variable may not also have a get specifier
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
//只重寫 get 則該屬性重寫為只讀屬性,不會調(diào)用 set 方法, 所以 重寫屬性觀察器的話會編譯錯誤
get {
return super.name + "in subclass"
}
willSet {
}
}
}
- 不能同時提供重寫的
setter和重寫的屬性觀察器:
編譯報錯:error: willSet variable may not also have a set specifier
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
get {
return super.name + "in subclass"
}
set {
name = newValue + "in subclass"
}
willSet {
print("will set")
}
}
}
正確的做法是,可以通過定制的 setter 觀察屬性的值變化
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
get {
return super.name + "in subclass"
}
set {
print("副作用...")
name = newValue + "in subclass"
}
}
}
防止重寫
通過把方法,屬性或下標標記為 final 來防止它們被重寫,只需要在聲明關(guān)鍵字前加上 final 修飾符即 可(例如: final var , final func , final class func , 以及 final subscript )。
如果你重寫了帶有 final 標記的方法,屬性或下標,在編譯時會報錯。在類擴展中的方法,屬性或下標也可以在 擴展的定義里標記為 final 的。
你可以通過在關(guān)鍵字 class 前添加 final 修飾符( final class )來將整個類標記為 final 的。這樣的類是不可 被繼承的,試圖繼承這樣的類會導(dǎo)致編譯報錯。
20.構(gòu)造過程
構(gòu)造過程 是使用類、結(jié)構(gòu)體或枚舉類型的實例之前的準備過程。在新實例可用之前必須執(zhí)行的過程,具體操作包括:
- 設(shè)置實例中每個存儲屬性的初始值
- 執(zhí)行其他必須的設(shè)置或者初始化工作
通過定義 構(gòu)造器 來實現(xiàn)構(gòu)造過程,這些構(gòu)造器可用看做是用來創(chuàng)建特定類型新實例的特殊方法。 Swift 的構(gòu)造器無需返回值,它們的主要任務(wù)是確保新實例在第一次使用之前完成正確的初始化
存儲屬性初始賦值
類和結(jié)構(gòu)體 在創(chuàng)建實例時,必須為所有存儲屬性設(shè)置合適的初始值。存儲屬性的值不能處于一個位置的狀態(tài)。
// 直接設(shè)置默認屬性值
class SomeClass {
var name: String = "haha"
}
// 類SomeClass沒有設(shè)置默認屬性值,編譯報錯 error: class 'SomeStruct' has no initializers
class SomeClass {
var name: String
}
// 可以在構(gòu)造器中設(shè)置默認屬性值
class SomeClass {
var name: String
init() {
name = "initialName"
}
}
// 直接設(shè)置默認屬性值
struct SomeStruct {
var name: String = "haha"
}
// 或通過默認構(gòu)造器設(shè)置
struct SomeStruct {
var name: String
}
// 結(jié)構(gòu)體SomeStruct 沒有設(shè)置默認屬性值,但是編譯器會自動生成一個逐一成員構(gòu)造器
let someStruct = SomeStruct(name: "name")
//也可以構(gòu)造器中設(shè)置默認屬性值
struct SomeStruct {
var name: String
init() {
name = "haha"
}
}
let someStruct = SomeStruct()
注意
在為存儲屬性設(shè)置默認值或者在構(gòu)造器中為其賦值時,它們的值是直接被設(shè)置的,不會觸發(fā)任何屬性觀察器
值類型的構(gòu)造器代理
構(gòu)造器可以通過調(diào)用其它構(gòu)造器來完成實例的部分構(gòu)造過程。這一過程稱為構(gòu)造器代理,它能減少多個構(gòu)造器間
的代碼重復(fù)。
構(gòu)造器代理的實現(xiàn)規(guī)則和形式在值類型和類類型中有所不同。值類型(結(jié)構(gòu)體和枚舉類型)不支持繼承,所以構(gòu)造器代理的過程相對簡單,因為它們只能代理給自己的其它構(gòu)造器。類則不同,它可以繼承自其它類這意味著類有責(zé)任保證其所有繼承的存儲型屬性在構(gòu)造時也能正確的初始化。
對于值類型,你可以使用 self.init 在自定義的構(gòu)造器中引用相同類型中的其它構(gòu)造器。并且你只能在構(gòu)造器內(nèi)部調(diào)用 self.init 。
如果你為某個值類型定義了一個自定義的構(gòu)造器,你將無法訪問到默認構(gòu)造器(如果是結(jié)構(gòu)體,還將無法訪問逐一成員構(gòu)造器)。這種限制可以防止你為值類型增加了一個額外的且十分復(fù)雜的構(gòu)造器之后,仍然有人錯誤的使用自動生成的構(gòu)造器。
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
// 使用默認屬性來初始化
init() {}
// 使用指定的 origin 和 size 來初始化
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
// 使用指定的 center 和 size 來初始化
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
注意
假如你希望默認構(gòu)造器、逐一成員構(gòu)造器以及你自己的自定義構(gòu)造器都能用來創(chuàng)建實例,可以將自定義的構(gòu)造器寫到擴展(extension)中,而不是寫在值類型的原始定義中。
struct Rect {
var origin = Point(x: 0.0, y: 0.0)
var size = Size(width: 0.0, height: 0.0)
}
extension Rect {
// 使用指定的 center 和 size 來初始化
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
// 默認構(gòu)造器
let r0 = Rect()
// 自定義構(gòu)造器
let r1 = Rect(center: Point(x: 1.0, y: 2.0), size: Size(width: 3.0, height: 4.0))
// 逐一構(gòu)造器
let r2 = Rect(origin: Point(x: 1.0, y: 2.0), size: Size(width: 3.0, height: 4.0))
21.類的繼承和構(gòu)造過程
類里面的所有存儲型屬性——包括所有繼承自父類的屬性——都必須在構(gòu)造過程中設(shè)置初始值。
指定構(gòu)造器
指定構(gòu)造器是類中最主要的構(gòu)造器,一個指定構(gòu)造器將初始化類中提供的所有屬性,并根據(jù)父類鏈往上調(diào)用父類的構(gòu)造器來實現(xiàn)父類的初始化。
每一個類都必須擁有至少一個指定構(gòu)造器。
便利構(gòu)造器
便利構(gòu)造器是類中比較次要的、輔助型的構(gòu)造器。你可以定義便利構(gòu)造器來調(diào)用同一個類中的指定構(gòu)造器,并未其參數(shù)提供默認值。也可以定義便利構(gòu)造器來創(chuàng)建一個特殊用途或特定輸入值的實例。
類的指定構(gòu)造器和便利構(gòu)造器語法
指定構(gòu)造器語法:
init(parameters) {
statements
}
便利構(gòu)造器語法: convenience
convenience init(parameters) {
statements
}
22.通過閉包或函數(shù)設(shè)置屬性的默認值
如果某個 存儲屬性 的默認值需要一些定制或設(shè)置,可以使用閉包或全局函數(shù)為其提供定制的默認值,每當(dāng)這個屬性所在的類型的新實例被創(chuàng)建時,對應(yīng)的閉包或函數(shù)會被調(diào)用,而他們的返回值會被當(dāng)做默認值賦值給這個屬性。
這種類型的閉包或函數(shù)通常會創(chuàng)建一個跟屬性類型相同的臨時變量,然后修改它的值以滿足預(yù)期的初始黃臺,最后返回這個臨時變量,作為屬性的默認值。
class SomeClass {
var name: String
init(name: String) {
self.name = name
}
}
class SomeSub: SomeClass {
var age: String = {
let age = "11"
print("age = \(age)")
return age
}()
}
let sub = SomeSub(name: "haha")
//age = 11
注意閉包結(jié)尾的大括號后面接了一對空的小括號。這用來告訴 Swift 立即執(zhí)行此閉包。如果你忽略了這對括號,相當(dāng)于將閉包本身作為值賦值給了屬性,而不是將閉包的返回值賦值給屬性。
23.解決實例之間的強引用循環(huán)
類實例之間的強引用循環(huán):
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 )")
}
}
var tom: Person?
var unit4A: Apartment?
tom = Person(name: "Tom")
unit4A = Apartment(unit: "4A")
tom?.apartment = unit4A
unit4A?.tenant = tom
tom = nil
unit4A = nil
//當(dāng)吧tom 和 unit4A 設(shè)置為nil 時,析構(gòu)函數(shù)均不會調(diào)用,強引用循環(huán)會一直阻止 Person 和 Apartment 類實例的銷毀,從而導(dǎo)致內(nèi)存泄漏
弱引用和無主引用
Swift 提供兩種方法來解決在使用類的屬性時所遇到的強引用循環(huán)問題:弱引用 (weak reference)和 無主引用 (unowned reference)
弱引用和無主引用允許引用循環(huán)中的一個實例引用另一個實例而不保持強引用。 這樣實例之間能相互引用而不產(chǎn)生強引用循環(huán)。
當(dāng)其他的實例有更短的生命周期時,使用弱引用,即當(dāng)其他實例先析構(gòu)時使用弱引用。 在上面的例子中, 在一個公寓的聲明周期內(nèi),會存在某段時間沒有主人的情況,所以在 Apartment 類中使用弱引用來打破循環(huán)引用。 相反的,當(dāng)其他實例擁有相同或者更長的聲明周期的情況下,應(yīng)該使用無主引用。
弱引用
弱引用指不會對其引用的實例保持強引用,因此不會阻止 ARC 銷毀被引用的實例。這個特性阻止了引用變?yōu)閺娨醚h(huán)的一部分。在聲明屬性或變量時,在前面加上 weak 關(guān)鍵字表明這是一個弱引用。
由于弱引用并不會保持所引用的實例,即使引用存在,實例也有可能被銷毀。因此,ARC 會在引用的實例被銷毀后,自動將其賦值為 nil。并且,因為弱引用允許他們的值在運行時被賦值為 nil, 所以他們會被定義為可選類型變量而不是常量。
注意
當(dāng) ARC 設(shè)置弱引用為nil時,屬性觀察器不會被觸發(fā)
將 Apartment 的 tenant 屬性聲明成弱引用后, Person 實例依然保持對 Apartment 實例的強引用,但是 Apartment 實例只持有對 Person 實例的弱引用。這意味著當(dāng)斷開 tom 變量所持有的強引用時,再也沒有指向 Person 實例的強引用了, 由于再也沒有指向 Person 實例的強引用,該實例 (tom) 會被銷毀
tom = nil
// Tom is being deinitialized
唯一剩下執(zhí)行 Apartment 實例的強引用來自 unit4A ,如果斷開這個強引用,再也沒有指向 Apartment 實例的強引用了, 該實例 (unit4A)也會被銷毀
unit4A = nil
//Apartment 4A is being deinitialized
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")
}
}
var tom: Person?
var unit4A: Apartment?
tom = Person(name: "Tom")
unit4A = Apartment(unit: "4A")
tom?.apartment = unit4A
unit4A?.tenant = tom
tom = nil
// Tom is being deinitialized
unit4A = nil
//Apartment 4A is being deinitialized
無主引用
和弱引用類似,無主引用保持對一個實例的強引用。和弱引用不同的是無主引用在其他實例有相同或者更長的生命周期時使用。你可以在聲明屬性或者變量時,在前面加上關(guān)鍵字 unowned 表示這是一個無主引用。
無主引用通常被期望擁有值。不過 ARC 無法再實例被銷毀后將無主引用設(shè)為 nil ,因為非可選類型的變量不允許被賦值為 nil 。
重要
使用無主引用,你必須確保引用始終指向一個未被銷毀的實例。
如果你視圖在實例被銷毀后,訪問該實例的無主引用,會觸發(fā)運行時錯誤。
下例定義了兩個類 Customer 和 CreditCard, 模擬了銀行客戶和信用卡。 這兩個類中,每一個都將和另外一個類的實例作為自身的屬性。這種關(guān)系可能會造成強引用循環(huán)。
Customer 和 CreditCard 之間的關(guān)系與之前弱引用例子中的 Apartment 和 Person 不同。 一個客戶可能有或者沒有信用卡,但是一張信用卡總是關(guān)聯(lián)值一個客戶。 為了表示這種關(guān)系, Customer 類有一個可選類型的 card 屬性,但是 CreditCard 類有一個非可選類型的 customer 屬性。
此外,只能通過將一個 number 值和 customer 實例傳遞給 CreditCard 的構(gòu)造函數(shù)的方式來創(chuàng)建 CreditCard 實例。 這樣可以確保當(dāng)創(chuàng)建 CreditCard 實例時總是有一個 customer 實例與之關(guān)聯(lián)。
由于信用卡總是關(guān)聯(lián)著一個客戶,因此將 customer 屬性定義為無主引用,可以避免引用循環(huán)
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var tom: Customer?
tom = Customer(name: "Tom")
tom?.card = CreditCard(number: 1234_5678_9012_3456, customer: tom!)
tom = nil
//Tom is being deinitialized
//Card #1234567890123456 is being deinitialized
在關(guān)聯(lián)兩個實例后, Customer 實例持有對 CreditCard 實例的強引用,而 CreditCard 實例持有對 Customer 實例的無主引用。
由于 customer 的無主醫(yī)用,當(dāng)斷開 tom 變量持有的強引用時,再也沒有指向 Customer 實例的強引用。由于再也沒有指向 Customer 實例的強引用,該實例會被銷毀。其后,再也沒有指向 CreditCard 實例的強引用,該實例也被隨之銷毀了。
tom = nil
//Tom is being deinitialized
//Card #1234567890123456 is being deinitialized
注意
上面的例子展示了如何使用 安全的無主引用 。對于需要禁用運行時的安全檢查情況(例如,出于性能方面的原因), Swift 還提供了 不安全的無主引用 。與所有不安全的操作一樣,你需要負責(zé)檢查代碼以確保其安全性。 可用通過unowned(unsafe)來聲明不安全無主引用。 如果你視圖在實例被銷毀后,訪問該實例的不安全無主引用,你的程序會嘗試訪問該實例之前所在的內(nèi)存地址,這是一個不安全的操作。
無主引用以及隱式解析可選屬性
上面兩個例子:
-
Person和Apartment的例子展示了兩個屬性都允許為nil,并會潛在的產(chǎn)生強引用循環(huán),這種場景適合用弱引用來解決 -
Customer和CreditCard的例子展示了一個屬性的值允許為nil, 而另一個屬性的值不允許為nil,這也可能會產(chǎn)生強引用循環(huán),這種場景適合通過無主引用來解決。
然而,存在著第三種場景,在這種場景中,兩個屬性都必須有值,并且初始化后永遠不會為 nil。在這種場景中,需要一個類使用無主類型,而另一個類使用隱式解析可選屬性。
這個兩個屬性在初始化完成后能被直接訪問 (不需要展開) ,同時避免了引用循環(huán)。
例如:
定義兩個類, Country 和 City,每個類都將另一個類的實例保存為屬性。在這個模型中,每個國家都必須有首都,每個城市都必須屬于一個國家。為了實現(xiàn)這種關(guān)系, Country 類擁有一個 capitalCity 屬性,而 City 類擁有一個 country 屬性:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
deinit {
print("city \(name) is being deinitialized")
}
}
class City {
let name: String
let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
deinit {
print("county: \(name) is being deinitialized")
}
}
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”
為了建立兩個類的依賴關(guān)系, City 的構(gòu)造含函數(shù)接受一個Country 實例作為參數(shù),并且將實例保存到 country 屬性。
Country 的構(gòu)造函數(shù)調(diào)用了 City 的構(gòu)造函數(shù)。然而,只有 Country 的實例完全初始化后, Country 的構(gòu)造函數(shù)才能把 self 傳給 City 的構(gòu)造函數(shù)。 (在兩段式構(gòu)造過程中有具體描述。)
// 將 capitalCity 屬性改為非隱式可選類型的話
class Country {
let name: String
var capitalCity: City
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
deinit {
print("city \(name) is being deinitialized")
}
}
// 編譯器報錯
//'self' used before all stored properties are initialized
// self.capitalCity = City(name: capitalName, country: self) 此時 self 沒有完成初始化
為了滿足這種需求,通過在類型結(jié)尾處加上感嘆號 City! 的方式,將 Country 的 capitalCity 屬性聲明為隱式解析可選類型的屬性。這意味像其他可選類型一樣, capitalCity屬性的默認值為 nil,單是不需要展開它的值就能訪問它。(在隱式解析可選類型中有描述。)
由于 capitalCity 默認值為 nil,一旦 Country 的實例在構(gòu)造函數(shù)中給 name 屬性賦值后,整個初始化過程就完成了。這意味著一旦 name 屬性被賦值后, Country 的構(gòu)造函數(shù)就能引用并傳遞隱式的 self 。 Country 的構(gòu)造函數(shù)在賦值 capitalCity 時就能將 self 作為參數(shù)傳遞給 City 的構(gòu)造函數(shù)。
// 如果不先初始化 name 則會報錯
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.capitalCity = City(name: capitalName, country: self)
self.name = name
}
deinit {
print("city \(name) is being deinitialized")
}
}
//error: 'self' used before all stored properties are initialized
//由于沒有完成初始化,不能講self 作為參數(shù)傳遞給 City的構(gòu)造函數(shù) self.capitalCity = City(name: capitalName, country: self)
閉包引起的強引用循環(huán)
強引用循環(huán)還會發(fā)生在當(dāng)你講一個閉包賦值給類實例的某個屬性,并且這個閉包中又捕獲(使用)了這個類實例時。這個閉包中可能訪問了實例的某個屬性,例如 self.somProperty ,或者閉包中調(diào)用了實例的某個方法,例如 self.someMethod() 。這兩種情況都導(dǎo)致了閉包捕獲 self ,從而產(chǎn)生了強引用循環(huán)。
強引用循環(huán)的產(chǎn)生,是因為閉包和類相似,都是引用類型。當(dāng)你把一個閉包賦值給某個屬性時,你是講這個閉包的引用賦值給了屬性。實質(zhì)上,這和之前的問題是一樣的 -- 兩個強引用彼此一直有效。但是,和兩個類實例不同,這次是一個實例,另一個是閉包。
閉包引起的強引用循環(huán)示例:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil
// 無輸出
實例的 paragraph 的 asHTML 屬性持有閉包的強引用,但是,閉包在其內(nèi)部引用了 self (引用了 self.name 和 self.text),因此閉包捕獲了 self,這意味著閉包反過來持有了 paragraph 的強引用。 這樣兩個對象就產(chǎn)生了強引用循環(huán)。
注意:
雖然閉包多次使用了self,它只捕獲HTMLElement實例的一個強引用。
如果設(shè)置 paragraph 變量為 nil, 打破它持有的 HTMLElement 實例的強引用, 由于強引用循環(huán),HTMLElement 實例和它的閉包都不會銷毀。
解決閉包引起的強引用循環(huán)
Swift 提供了一種優(yōu)雅的方式來解決閉包引起的強引用循環(huán)問題,稱為 閉包捕獲列表 (closure capture list)
在定義閉包的同時,定義捕獲列表作為閉包的一部分,通過這種方式可以解決閉包和類實例之間的強引用循環(huán)。捕獲列表定義了閉包體內(nèi)捕獲一個或者多個引用類型的規(guī)則。和類實例之間的強引用循環(huán)一樣,聲明每個捕獲的引用為弱引用或無主引用來替代強引用。根據(jù)代碼關(guān)系來決定使用弱引用還是無主引用。
注意
Swift 要求只要在閉包內(nèi)使用了self的成員,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())。這提醒你可能一不小心就捕獲了self。
定義捕獲列表
駁貨列表的每一項都由一對元素組成,一個元素是 weak 或 unowned 關(guān)鍵字,另一個元素是類實例的引用(如 self) 或初始化過的變量 (如 delegate = self.delegate!)。這些項在方括號中用逗號分開。
如果閉包有參數(shù)列表和返回類型,把捕獲列表放在它們前面:
lazy var someClousure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProress: String) -> String in
// 這里是閉包的函數(shù)體
}
如果閉包沒有知名參數(shù)或返回類型,即它們會通過上下文推斷,那么可以把捕獲列表和關(guān)鍵字 in 放在閉包最開始的地方:
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in // 這里是閉包的函數(shù)體
}
弱引用和無主引用
在閉包和捕獲的實例總是相互引用并且總是同時銷毀時,將閉包內(nèi)的捕獲值定義為 無主引用
相反的,在被捕獲的引用可能會變成 nil 時,將閉包內(nèi)的捕獲定義為 弱引用。 弱引用總是可選類型,并且當(dāng)引用的實例被銷毀后,弱引用的值會自動置為 nil。這使我們可以在閉包內(nèi)檢查它們是否存在。
注意
如果被捕獲的引用絕對不會變?yōu)?nil,應(yīng)該用無主引用,而不是弱引用。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil
//p is being deinitialized
在 asHTML 閉包中通過添加捕獲列表 [unowned self] 來表示將 self 捕獲為無主引用而不是強引用,這樣閉包以無主引用的方式捕獲 self,并不會持有 HTMLElement 實例的強引用。 將 paragraph 置為 nil 后, HTMLElement 實例將會被銷毀。
Any 和 AnyObject 的類型轉(zhuǎn)換
Swift 為不確定類型提供了兩種特殊的類型別名:
- Any 可以表示任何類型,包括函數(shù)類型。
- AnyObject 可以表示任何類類型的實例。
重載運算符使用static修飾:
Swift提供了類似于C++中的重載運算符方法,可以對自定義類型實現(xiàn)運算符操作。
在定義的時候,運算符函數(shù)都是static修飾的,因為運算符函數(shù)是類方法(或者結(jié)構(gòu),枚舉)。