本章將會介紹
閉包表達式
尾隨閉包
值捕獲
閉包是引用類型
逃逸閉包
自動閉包
枚舉語法
使用Switch語句匹配枚舉值
關聯值
原始值
遞歸枚舉
閉包
閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些編程語言中的匿名函數比較相似。
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用。被稱為包裹常量和變量。 Swift 會為你管理在捕獲過程中涉及到的所有內存操作。
在函數章節中介紹的全局和嵌套函數實際上也是特殊的閉包,閉包采取如下三種形式之一:
- 全局函數是一個有名字但不會捕獲任何值的閉包
- 嵌套函數是一個有名字并可以捕獲其封閉函數域內值的閉包
- 閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包
Swift 的閉包表達式擁有簡潔的風格,并鼓勵在常見場景中進行語法優化,主要優化如下:
- 利用上下文推斷參數和返回值類型
- 隱式返回單表達式閉包,即單表達式閉包可以省略 return 關鍵字
- 參數名稱縮寫
- 尾隨閉包語法
1.閉包表達式
嵌套函數是一個在較復雜函數中方便進行命名和定義自包含代碼模塊的方式。當然,有時候編寫小巧的沒有完整定義和命名的類函數結構也是很有用處的,尤其是在你處理一些函數并需要將另外一些函數作為該函數的參數時。
閉包表達式是一種利用簡潔語法構建內聯閉包的方式。閉包表達式提供了一些語法優化,使得撰寫閉包變得簡單明了。下面閉包表達式的例子通過使用幾次迭代展示了 sorted(by:) 方法定義和語法優化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。
- sorted方法
Swift 標準庫提供了名為 sorted(by:) 的方法,它會根據你所提供的用于排序的閉包函數將已知類型數組中的值進行排序。一旦排序完成,sorted(by:) 方法會返回一個與原數組大小相同,包含同類型元素且元素已正確排序的新數組。原數組不會被 sorted(by:) 方法修改。
下面的閉包表達式示例使用 sorted(by:) 方法對一個 String 類型的數組進行字母逆序排序。以下是初始數組:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:) 方法接受一個閉包,該閉包函數需要傳入與數組元素類型相同的兩個值,并返回一個布爾類型值來表明當排序結束后傳入的第一個參數排在第二個參數前面還是后面。如果第一個參數值出現在第二個參數值前面,排序閉包函數需要返回true,反之返回false。
該例子對一個 String 類型的數組進行排序,因此排序閉包函數類型需為 (String, String) -> Bool。
提供排序閉包函數的一種方式是撰寫一個符合其類型要求的普通函數,并將其作為 sorted(by:) 方法的參數傳入:
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames 為 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一個字符串(s1)大于第二個字符串(s2),backward(_:_:)
函數會返回 true,表示在新的數組中 s1 應該出現在 s2 前。對于字符串中的字符來說,“大于”表示“按照字母順序較晚出現”。這意味著字母 "B" 大于字母 "A" ,字符串 "Tom" 大于字符串 "Tim"。該閉包將進行字母逆序排序,"Barry" 將會排在 "Alex" 之前。
然而,以這種方式來編寫一個實際上很簡單的表達式(a > b),確實太過繁瑣了。對于這個例子來說,利用閉包表達式語法可以更好地構造一個內聯排序閉包。
- 閉包表達式語法
閉包表達式語法有如下的一般形式:
{ (parameters) -> returnType in
statements
}
閉包表達式參數 可以是 in-out 參數,但不能設定默認值。也可以使用具名的可變參數(譯者注:但是如果可變參數不放在參數列表的最后一位的話,調用閉包的時時編譯器將報錯。可參考這里)。元組也可以作為參數和返回值。
下面的例子展示了之前 backward(_:_:)
函數對應的閉包表達式版本的代碼:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是內聯閉包參數和返回值類型聲明與 backward(::) 函數類型聲明相同。在這兩種方式中,都寫成了 (s1: String, s2: String) -> Bool。然而在內聯閉包表達式中,函數和返回值類型都寫在大括號內,而不是大括號外。
閉包的函數體部分由關鍵字in引入。該關鍵字表示閉包的參數和返回值類型定義已經完成,閉包函數體即將開始。
由于這個閉包的函數體部分如此短,以至于可以將其改寫成一行代碼:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
該例中 sorted(by:) 方法的整體調用保持不變,一對圓括號仍然包裹住了方法的整個參數。然而,參數現在變成了內聯閉包。
- 根據上下文推斷類型
因為排序閉包函數是作為 sorted(by:) 方法的參數傳入的,Swift 可以推斷其參數和返回值的類型。sorted(by:) 方法被一個字符串數組調用,因此其參數必須是 (String, String) -> Bool 類型的函數。這意味著 (String, String) 和 Bool 類型并不需要作為閉包表達式定義的一部分。因為所有的類型都可以被正確推斷,返回箭頭(->)和圍繞在參數周圍的括號也可以被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
實際上,通過內聯閉包表達式構造的閉包作為參數傳遞給函數或方法時,總是能夠推斷出閉包的參數和返回值類型。這意味著閉包作為函數或者方法的參數時,你幾乎不需要利用完整格式構造內聯閉包。
盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,則我們更鼓勵采用完整格式的閉包。而在 sorted(by:) 方法這個例子里,顯然閉包的目的就是排序。由于這個閉包是為了處理字符串數組的排序,因此讀者能夠推測出這個閉包是用于字符串處理的。
- 單表達式閉包隱式返回
單行表達式閉包可以通過省略 return 關鍵字來隱式返回單行表達式的結果,如上版本的例子可以改寫為:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
在這個例子中,sorted(by:) 方法的參數類型明確了閉包必須返回一個 Bool 類型值。因為閉包函數體只包含了一個單一表達式(s1 > s2),該表達式返回 Bool 類型值,因此這里沒有歧義,return 關鍵字可以省略。
- 參數名稱縮寫
Swift 自動為內聯閉包提供了參數名稱縮寫功能,你可以直接通過 $0
,$1
,$2
來順序調用閉包的參數,以此類推。
如果你在閉包表達式中使用參數名稱縮寫,你可以在閉包定義中省略參數列表,并且對應參數名稱縮寫的類型會通過函數類型進行推斷。in關鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數體構成:
reversedNames = names.sorted(by: { $0 > $1 } )
在這個例子中,$0和$1表示閉包中第一個和第二個 String 類型的參數。
- 運算符方法
實際上還有一種更簡短的方式來編寫上面例子中的閉包表達式。Swift 的 String 類型定義了關于大于號(>)的字符串實現,其作為一個函數接受兩個 String 類型的參數并返回 Bool 類型的值。而這正好與 sorted(by:) 方法的參數需要的函數類型相符合。因此,你可以簡單地傳遞一個大于號,Swift 可以自動推斷出你想使用大于號的字符串函數實現:
reversedNames = names.sorted(by: >)
2.尾隨閉包
如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括號之后的閉包表達式,函數支持將其作為最后一個參數調用。在使用尾隨閉包時,你不用寫出它的參數標簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函數體部分
}
// 以下是不使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure(closure: {
// 閉包主體部分
})
// 以下是使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
在閉包表達式語法一節中作為 sorted(by:) 方法參數的字符串排序閉包可以改寫為:
reversedNames = names.sorted() { $0 > $1 }
如果閉包表達式是函數或方法的唯一參數,則當你使用尾隨閉包時,你甚至可以把 () 省略掉:
reversedNames = names.sorted { $0 > $1 }
當閉包非常長以至于不能在一行中進行書寫時,尾隨閉包變得非常有用。舉例來說,Swift 的 Array 類型有一個 map(_:) 方法,這個方法獲取一個閉包表達式作為其唯一參數。該閉包函數會為數組中的每一個元素調用一次,并返回該元素所映射的值。具體的映射方式和返回值類型由閉包來指定。
當提供給數組的閉包應用于每個數組元素后,map(_:) 方法將返回一個新的數組,數組中包含了與原數組中的元素一一對應的映射后的值。
下例介紹了如何在 map(_:) 方法中使用尾隨閉包將 Int 類型數組 [16, 58, 510] 轉換為包含對應 String 類型的值的數組["OneSix", "FiveEight", "FiveOneZero"]:
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]
如上代碼創建了一個整型數位和它們英文版本名字相映射的字典。同時還定義了一個準備轉換為字符串數組的整型數組。
你現在可以通過傳遞一個尾隨閉包給 numbers 數組的 map(_:) 方法來創建對應的字符串版本數組:
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings 常量被推斷為字符串類型數組,即 [String]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]
map(_:) 為數組中每一個元素調用了一次閉包表達式。你不需要指定閉包的輸入參數 number 的類型,因為可以通過要映射的數組類型進行推斷。
在該例中,局部變量 number 的值由閉包中的 number 參數獲得,因此可以在閉包函數體內對其進行修改,(閉包或者函數的參數總是常量),閉包表達式指定了返回類型為 String,以表明存儲映射值的新數組類型為 String。
閉包表達式在每次被調用的時候創建了一個叫做 output 的字符串并返回。其使用求余運算符(number % 10)計算最后一位數字并利用 digitNames 字典獲取所映射的字符串。這個閉包能夠用于創建任意正整數的字符串表示。
注意:
字典 digitNames 下標后跟著一個嘆號(!),因為字典下標返回一個可選值(optional value),表明該鍵不存在時會查找失敗。在上例中,由于可以確定 number % 10 總是 digitNames 字典的有效下標,因此嘆號可以用于強制解包 (force-unwrap) 存儲在下標的可選類型的返回值中的String類型的值。
從 digitNames 字典中獲取的字符串被添加到 output 的前部,逆序建立了一個字符串版本的數字。(在表達式 number % 10 中,如果 number 為 16,則返回 6,58 返回 8,510 返回 0。)
number 變量之后除以 10。因為其是整數,在計算過程中未除盡部分被忽略。因此 16 變成了 1,58 變成了 5,510 變成了 51。
整個過程重復進行,直到 number /= 10 為 0,這時閉包會將字符串 output 返回,而 map(_:) 方法則會將字符串添加到映射數組中。
在上面的例子中,通過尾隨閉包語法,優雅地在函數后封裝了閉包的具體功能,而不再需要將整個閉包包裹在 map(_:) 方法的括號內。
3.值捕獲
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
Swift 中,可以捕獲值的閉包的最簡單形式是嵌套函數,也就是定義在其他函數的函數體內的函數。嵌套函數可以捕獲其外部函數所有的參數以及定義的常量和變量。
舉個例子,這有一個叫做 makeIncrementer 的函數,其包含了一個叫做 incrementer 的嵌套函數。嵌套函數 incrementer() 從上下文中捕獲了兩個值,runningTotal 和 amount。捕獲這些值之后,makeIncrementer 將 incrementer 作為閉包返回。每次調用 incrementer 時,其會以 amount 作為增量增加 runningTotal 的值。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer 返回類型為 () -> Int。這意味著其返回的是一個函數,而非一個簡單類型的值。該函數在每次調用時不接受參數,只返回一個 Int 類型的值。
makeIncrementer(forIncrement:) 函數定義了一個初始值為 0 的整型變量 runningTotal,用來存儲當前總計數值。該值為 incrementer 的返回值。
makeIncrementer(forIncrement:) 有一個 Int 類型的參數,其外部參數名為 forIncrement,內部參數名為 amount,該參數表示每次 incrementer 被調用時 runningTotal 將要增加的量。makeIncrementer 函數還定義了一個嵌套函數 incrementer,用來執行實際的增加操作。該函數簡單地使 runningTotal 增加 amount,并將其返回。
如果我們單獨考慮嵌套函數 incrementer(),會發現它有些不同尋常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer() 函數并沒有任何參數,但是在函數體內訪問了 runningTotal 和 amount 變量。這是因為它從外圍函數捕獲了 runningTotal 和 amount 變量的引用。捕獲引用保證了 runningTotal 和 amount 變量在調用完 makeIncrementer 后不會消失,并且保證了在下一次執行 incrementer 函數時,runningTotal 依舊存在。
注意 為了優化,如果一個值不會被閉包改變,或者在閉包創建后不會改變,Swift 可能會改為捕獲并保存一份對值的拷貝。 Swift 也會負責被捕獲變量的所有內存管理工作,包括釋放不再需要的變量。
下面是一個使用 makeIncrementer 的例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
該例子定義了一個叫做 incrementByTen 的常量,該常量指向一個每次調用會將其 runningTotal 變量增加 10 的 incrementer 函數。調用這個函數多次可以得到以下結果:
incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30
如果你創建了另一個 incrementer,它會有屬于自己的引用,指向一個全新、獨立的 runningTotal 變量:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值為7
再次調用原來的 incrementByTen 會繼續增加它自己的 runningTotal 變量,該變量和 incrementBySeven 中捕獲的變量沒有任何聯系:
incrementByTen()
// 返回的值為40
注意:
如果你將閉包賦值給一個類實例的屬性,并且該閉包通過訪問該實例或其成員而捕獲了該實例,你將在閉包和該實例間創建一個循環強引用。Swift 使用捕獲列表來打破這種循環強引用。
4.閉包是引用類型
上面的例子中,incrementBySeven 和 incrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因為函數和閉包都是引用類型。
無論你將函數或閉包賦值給一個常量還是變量,你實際上都是將常量或變量的值設置為對應函數或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen 是一個常量,而并非閉包內容本身。
這也意味著如果你將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50
5.逃逸閉包
當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。當你定義接受閉包作為參數的函數時,你可以在參數名之前標注 @escaping,用來指明這個閉包是允許“逃逸”出這個函數的。
一種能使閉包“逃逸”出函數的方法是,將這個閉包保存在一個函數外部定義的變量中。舉個例子,很多啟動異步操作的函數接受一個閉包參數作為 completion handler。這類函數會在異步操作開始之后立刻返回,但是閉包直到異步操作結束后才會被調用。在這種情況下,閉包需要“逃逸”出函數,因為閉包需要在函數返回之后被調用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函數接受一個閉包作為參數,該閉包被添加到一個函數外定義的數組中。如果你不將這個參數標記為 @escaping,就會得到一個編譯錯誤。
將一個閉包標記為 @escaping 意味著你必須在閉包中顯式地引用 self。比如說,在下面的代碼中,傳遞到 someFunctionWithEscapingClosure(_:)
中的閉包是一個逃逸閉包,這意味著它需要顯式地引用 self。相對的,傳遞到 someFunctionWithNonescapingClosure(_:)
中的閉包是一個非逃逸閉包,這意味著它可以隱式引用 self。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出 "200"
completionHandlers.first?()
print(instance.x)
// 打印出 "100"
6.自動閉包
自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。
我們經常會調用采用自動閉包的函數,但是很少去實現這樣的函數。舉個例子來說,assert(condition:message:file:line:) 函數接受自動閉包作為它的 condition 參數和 message 參數;它的 condition 參數僅會在 debug 模式下被求值,它的 message 參數僅當 condition 參數為 false 時被計算求值。
自動閉包讓你能夠延遲求值,因為直到你調用這個閉包,代碼段才會被執行。延遲求值對于那些有副作用(Side Effect)和高計算成本的代碼來說是很有益處的,因為它使得你能控制代碼的執行時機。下面的代碼展示了閉包如何延時求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
盡管在閉包的代碼中,customersInLine 的第一個元素被移除了,不過在閉包被調用之前,這個元素是不會被移除的。如果這個閉包永遠不被調用,那么在閉包里面的表達式將永遠不會執行,那意味著列表中的元素永遠不會被移除。請注意,customerProvider 的類型不是 String,而是 () -> String,一個沒有參數且返回值為 String 的函數。
將閉包作為參數傳遞給函數時,你能獲得同樣的延時求值行為。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出 "Now serving Alex!"
上面的 serve(customer:) 函數接受一個返回顧客名字的顯式的閉包。下面這個版本的 serve(customer:) 完成了相同的操作,不過它并沒有接受一個顯式的閉包,而是通過將參數標記為 @autoclosure 來接收一個自動閉包。現在你可以將該函數當作接受 String 類型參數(而非閉包)的函數來調用。customerProvider 參數將自動轉化為一個閉包,因為該參數被標記了 @autoclosure 特性。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印 "Now serving Ewa!"
注意 過度使用 autoclosures 會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。
如果你想讓一個自動閉包可以“逃逸”,則應該同時使用 @autoclosure 和 @escaping 屬性。
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// 打印 "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印 "Now serving Barry!"
// 打印 "Now serving Daniella!"
在上面的代碼中,collectCustomerProviders(_:) 函數并沒有調用傳入的 customerProvider 閉包,而是將閉包追加到了 customerProviders 數組中。這個數組定義在函數作用域范圍外,這意味著數組內的閉包能夠在函數返回之后被調用。因此,customerProvider 參數必須允許“逃逸”出函數作用域。
7.閉包總結
// 閉包
// sorted方法 需要傳入一個 (String, String) -> Bool 類型的參數
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reverseNames = names.sorted(by: backward)
reverseNames
// 閉包表達式語法
var reverseNames2 = names.sorted(by: {(s1: String, s2: String) -> Bool in
return s1 > s2
})
// 根據上下文推斷
var reverseNames3 = names.sorted(by: {s1, s2 in return s1 > s2 })
reverseNames3
// 單表達式閉包隱式返回
var reverseNames4 = names.sorted(by: {s1, s2 in s1 > s2})
reverseNames4
// 參數名稱縮寫
var reverseNames5 = names.sorted(by: { $0 > $1})
reverseNames5
// 運算符方法
var reverseNames6 = names.sorted(by: >)
reverseNames6
// 尾隨閉包
reverseNames6 = names.sorted(){$0 > $1}
reverseNames6
// 如果閉包表達式是函數或者方法的唯一參數,當使用尾隨閉包的時候可以把()省略
reverseNames6 = names.sorted{ $0 > $1 }
reverseNames6
// 當閉包非常長以至于不能在一行中進行的時候,尾隨閉包就非常有用了
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]
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
strings
// 值捕獲
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
print(runningTotal)
runningTotal += amount
return runningTotal
}
return incrementer
}
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementByTen()
incrementByTen()
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
incrementBySeven()
incrementByTen()
// 閉包是引用類型的
let anotherIncrementByTen = incrementByTen
anotherIncrementByTen()
// 逃逸閉包 當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
self.x = 100
}
someFunctionWithNonescapingClosure {
x = 200
}
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
completionHandlers.first?()
print(instance.x)
// 自動閉包 是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當被調用的時候,會返回被包裝在其中的表達式的值 自動閉包能夠延遲求值,能控制代碼的執行時機
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
let customerProvider = {
customersInLine.remove(at: 0)
}
// 在閉包調用之前元素不會從數組中移除
print(customersInLine.count)
customerProvider()
print(customersInLine.count)
// 將閉包作為參數傳遞給函數時,也能獲得同樣的延時求值行為
func serve(customer customerProvider: @autoclosure () -> String) {
customerProvider()
}
serve(customer: customersInLine.remove(at: 0))
print(customersInLine.count)
枚舉
枚舉為一組相關的值定義了一個共同的類型,使你可以在你的代碼中以類型安全的方式來使用這些值。
如果你熟悉 C 語言,你會知道在 C 語言中,枚舉會為一組整型值分配相關聯的名稱。Swift 中的枚舉更加靈活,不必給每一個枚舉成員提供一個值。如果給枚舉成員提供一個值(稱為“原始”值),則該值的類型可以是字符串,字符,或是一個整型值或浮點數。
此外,枚舉成員可以指定任意類型的關聯值存儲到枚舉成員中,就像其他語言中的聯合體(unions)和變體(variants)。你可以在一個枚舉中定義一組相關的枚舉成員,每一個枚舉成員都可以有適當類型的關聯值。
在 Swift 中,枚舉類型是一等(first-class)類型。它們采用了很多在傳統上只被類(class)所支持的特性,例如計算屬性(computed properties),用于提供枚舉值的附加信息,實例方法(instance methods),用于提供和枚舉值相關聯的功能。枚舉也可以定義構造函數(initializers)來提供一個初始值;可以在原始實現的基礎上擴展它們的功能;還可以遵循協議(protocols)來提供標準的功能。
1.枚舉方法
使用enum關鍵詞來創建枚舉并且把它們的整個定義放在一對大括號內:
enum SomeEnumeration {
// 枚舉定義放在這里
}
下面是用枚舉表示指南針四個方向的例子
enum CompassPoint {
case north
case south
case east
case west
}
枚舉中定義的值(如 north,south,east和west)是這個枚舉的成員值(或成員)。你可以使用case關鍵字來定義一個新的枚舉成員值。
注意
與 C 和 Objective-C 不同,Swift 的枚舉成員在被創建時不會被賦予一個默認的整型值。在上面的CompassPoint例子中,north,south,east和west不會被隱式地賦值為0,1,2和3。相反,這些枚舉成員本身就是完備的值,這些值的類型是已經明確定義好的CompassPoint類型。
多個成員值可以出現在同一行上,用逗號隔開:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
每個枚舉定義了一個全新的類型。像 Swift 中其他類型一樣,它們的名字(例如CompassPoint和Planet)應該以一個大寫字母開頭。給枚舉類型起一個單數名字而不是復數名字,以便于讀起來更加容易理解:
var directionToHead = CompassPoint.west
directionToHead的類型可以在它被CompassPoint的某個值初始化時推斷出來。一旦directionToHead被聲明為CompassPoint類型,你可以使用更簡短的點語法將其設置為另一個CompassPoint的值:
directionToHead = .east
當directionToHead的類型已知時,再次為其賦值可以省略枚舉類型名。在使用具有顯式類型的枚舉值時,這種寫法讓代碼具有更好的可讀性。
2.使用Switch語句匹配枚舉值
你可以使用switch語句匹配單個枚舉值:
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 打印 "Watch out for penguins”
你可以這樣理解這段代碼:
“判斷directionToHead的值。當它等于.north,打印“Lots of planets have a north”。當它等于.south,打印“Watch out for penguins”。”……以此類推。
正如在控制流中介紹的那樣,在判斷一個枚舉類型的值時,switch語句必須窮舉所有情況。如果忽略了.west這種情況,上面那段代碼將無法通過編譯,因為它沒有考慮到CompassPoint的全部成員。強制窮舉確保了枚舉成員不會被意外遺漏。
當不需要匹配每個枚舉成員的時候,你可以提供一個default分支來涵蓋所有未明確處理的枚舉成員:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// 打印 "Mostly harmless”
3.關聯值
上一小節的例子演示了如何定義和分類枚舉的成員。你可以為Planet.earth設置一個常量或者變量,并在賦值之后查看這個值。然而,有時候能夠把其他類型的關聯值和成員值一起存儲起來會很有用。這能讓你連同成員值一起存儲額外的自定義信息,并且你每次在代碼中使用該枚舉成員時,還可以修改這個關聯值。
你可以定義 Swift 枚舉來存儲任意類型的關聯值,如果需要的話,每個枚舉成員的關聯值類型可以各不相同。枚舉的這種特性跟其他語言中的可識別聯合(discriminated unions),標簽聯合(tagged unions),或者變體(variants)相似。
例如,假設一個庫存跟蹤系統需要利用兩種不同類型的條形碼來跟蹤商品。有些商品上標有使用0到9的數字的 UPC 格式的一維條形碼。每一個條形碼都有一個代表“數字系統”的數字,該數字后接五位代表“廠商代碼”的數字,接下來是五位代表“產品代碼”的數字。最后一個數字是“檢查”位,用來驗證代碼是否被正確掃描:

其他商品上標有 QR 碼格式的二維碼,它可以使用任何 ISO 8859-1 字符,并且可以編碼一個最多擁有 2,953 個字符的字符串:

這便于庫存跟蹤系統用包含四個整型值的元組存儲 UPC 碼,以及用任意長度的字符串儲存 QR 碼。
在 Swift 中,使用如下方式定義表示兩種商品條形碼的枚舉:
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
以上代碼可以這么理解:“定義一個名為Barcode的枚舉類型,它的一個成員值是具有(Int,Int,Int,Int)類型關聯值的upc,另一個成員值是具有String類型關聯值的qrCode。”
這個定義不提供任何Int或String類型的關聯值,它只是定義了,當Barcode常量和變量等于Barcode.upc或Barcode.qrCode時,可以存儲的關聯值的類型。然后可以使用任意一種條形碼類型創建新的條形碼,例如:
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
上面的例子創建了一個名為productBarcode的變量,并將Barcode.upc賦值給它,關聯的元組值為(8, 85909, 51226, 3)。
同一個商品可以被分配一個不同類型的條形碼,例如:
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
這時,原始的Barcode.upc和其整數關聯值被新的Barcode.qrCode和其字符串關聯值所替代。Barcode類型的常量和變量可以存儲一個.upc或者一個.qrCode(連同它們的關聯值),但是在同一時間只能存儲這兩個值中的一個。
像先前那樣,可以使用一個 switch 語句來檢查不同的條形碼類型。然而,這一次,關聯值可以被提取出來作為 switch 語句的一部分。你可以在switch的 case 分支代碼中提取每個關聯值作為一個常量(用let前綴)或者作為一個變量(用var前綴)來使用:
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."
如果一個枚舉成員的所有關聯值都被提取為常量,或者都被提取為變量,為了簡潔,你可以只在成員名稱前標注一個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."
4.原始值
在關聯值小節的條形碼例子中,演示了如何聲明存儲不同類型關聯值的枚舉成員。作為關聯值的替代選擇,枚舉成員可以被默認值(稱為原始值)預填充,這些原始值的類型必須相同。
這是一個使用 ASCII 碼作為原始值的枚舉:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
枚舉類型ASCIIControlCharacter的原始值類型被定義為Character,并設置了一些比較常見的 ASCII 控制字符。Character的描述詳見字符串和字符部分。
原始值可以是字符串,字符,或者任意整型值或浮點型值。每個原始值在枚舉聲明中必須是唯一的。
注意
原始值和關聯值是不同的。原始值是在定義枚舉時被預先填充的值,像上述三個 ASCII 碼。對于一個特定的枚舉成員,它的原始值始終不變。關聯值是創建一個基于枚舉成員的常量或變量時才設置的值,枚舉成員的關聯值可以變化。
- 原始值的隱式賦值
在使用原始值為整數或者字符串類型的枚舉時,不需要顯式地為每一個枚舉成員設置原始值,Swift 將會自動為你賦值。例如,當使用整數作為原始值時,隱式賦值的值依次遞增1。如果第一個枚舉成員沒有設置原始值,其原始值將為0。
下面的枚舉是對之前Planet這個枚舉的一個細化,利用整型的原始值來表示每個行星在太陽系中的順序:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
在上面的例子中,Plant.mercury的顯式原始值為1,Planet.venus的隱式原始值為2,依次類推。
當使用字符串作為枚舉類型的原始值時,每個枚舉成員的隱式原始值為該枚舉成員的名稱。下面的例子是CompassPoint枚舉的細化,使用字符串類型的原始值來表示各個方向的名稱:
enum CompassPoint: String {
case north, south, east, west
}
上面例子中,CompassPoint.south擁有隱式原始值south,依次類推。
使用枚舉成員的rawValue屬性可以訪問該枚舉成員的原始值:
let earthsOrder = Planet.earth.rawValue
// earthsOrder 值為 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection 值為 "west"
- 使用原始值初始化枚舉實例
如果在定義枚舉類型的時候使用了原始值,那么將會自動獲得一個初始化方法,這個方法接收一個叫做rawValue的參數,參數類型即為原始值類型,返回值則是枚舉成員或nil。你可以使用這個初始化方法來創建一個新的枚舉實例。
這個例子利用原始值7創建了枚舉成員uranus:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 類型為 Planet? 值為 Planet.uranus
然而,并非所有Int值都可以找到一個匹配的行星。因此,原始值構造器總是返回一個可選的枚舉成員。在上面的例子中,possiblePlanet是Planet?類型,或者說“可選的Planet”。
注意
原始值構造器是一個可失敗構造器,因為并不是每一個原始值都有與之對應的枚舉成員。
如果你試圖尋找一個位置為11的行星,通過原始值構造器返回的可選Planet值將是nil:
let positionToFind = 11
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 11
這個例子使用了可選綁定(optional binding),試圖通過原始值11來訪問一個行星。if let somePlanet = Planet(rawValue: 11)語句創建了一個可選Planet,如果可選Planet的值存在,就會賦值給somePlanet。在這個例子中,無法檢索到位置為11的行星,所以else分支被執行。
5.遞歸枚舉
遞歸枚舉是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上indirect來表示該成員可遞歸。
例如,下面的例子中,枚舉類型存儲了簡單的算術表達式:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
你也可以在枚舉類型開頭加上indirect關鍵字來表明它的所有成員都是可遞歸的:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
上面定義的枚舉類型可以存儲三種算術表達式:純數字、兩個表達式相加、兩個表達式相乘。枚舉成員addition和multiplication的關聯值也是算術表達式——這些關聯值使得嵌套表達式成為可能。例如,表達式(5 + 4) * 2,乘號右邊是一個數字,左邊則是另一個表達式。因為數據是嵌套的,因而用來存儲數據的枚舉類型也需要支持這種嵌套——這意味著枚舉類型需要支持遞歸。下面的代碼展示了使用ArithmeticExpression這個遞歸枚舉創建表達式(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))
要操作具有遞歸性質的數據結構,使用遞歸函數是一種直截了當的方式。例如,下面是一個對算術表達式求值的函數:
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"
該函數如果遇到純數字,就直接返回該數字的值。如果遇到的是加法或乘法運算,則分別計算左邊表達式和右邊表達式的值,然后相加或相乘。
6.枚舉總結
// 枚舉
// 枚舉語法
enum CompassPoint {
case north, south, east
case west
}
var directionHead = CompassPoint.east
// 一旦被聲明為確定的類型 則可以使用點語法來繼續設值
directionHead = .west
// 使用Switch語句匹配枚舉值 要保證枚舉的完備性 如果有不確定的項, 使用default
directionHead = .south
switch directionHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 關聯值
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
// 新建條形碼
var productBarcode = Barcode.upc(8, 123, 89, 10)
productBarcode = .qrCode("shhhshshshsh")
// 提取關聯值作為switch語句的一部分 也可以在switch的case語句中提取
switch productBarcode {
case .upc(let numberSystem, let manufacture, let product, let check):
print("UPC: \(numberSystem), \(manufacture), \(product), \(check)")
case .qrCode(let productCode):
print("QR code: \(productCode)")
}
switch productBarcode {
case let .upc(numberSystem, manufacture, product, check):
print("UPC: \(numberSystem), \(manufacture), \(product), \(check)")
case .qrCode(let productCode):
print("QR code: \(productCode)")
}
// 原始值
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
// 使用rawValue訪問原始值
Planet.earth.rawValue
enum CompassPoint2: String {
case north, south, east, west
}
CompassPoint2.west.rawValue
// 使用原始值初始化枚舉實例
let possiblePlanet = Planet(rawValue: 7)
possiblePlanet
let positionToFind = 11
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)")
}
// 遞歸枚舉 使用indirect關鍵字
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case mutiplication(ArithmeticExpression, ArithmeticExpression)
}
// 也可以在開頭加上indirect關鍵字來表明它的所有成員都是可遞歸的
indirect enum ArithmeticExpression2 {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case mutiplication(ArithmeticExpression, ArithmeticExpression)
}
// 計算(5 + 4)x 2
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.mutiplication(sum, ArithmeticExpression.number(2))
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 .mutiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product))