閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊 (blocks) 以及其他一些編程語言中的匿名函數(shù)比較相似。
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。
閉包表達式
在 Swift 中可以通過 func 關鍵字 定義一個函數(shù):
func sum(_ v1: Int, _ v2: Int) -> Int {
return v1 + v2
}
sum 函數(shù)調用:
sum(10, 20) // 30
也可以用過閉包表達式定義一個函數(shù)
var fn = { (v1: Int, v2: Int) -> Int in
return v1 + v2
}
閉包表達式調用:
fn(10, 20) // 30
在 Swift 中函數(shù)是一類公民,可以作為參數(shù)、返回值,跟 Int、Array、Class 等沒有區(qū)別:
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
觀察 exec 函數(shù),這是有三個參數(shù)的無返回值函數(shù):
- 第1個參數(shù):v1,類型為
Int - 第2個參數(shù):v2,類型為
Int - 第1個參數(shù):fn,類型為
(Int, Int) -> Int) - 無返回值
調用 exec 函數(shù):
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
// 注意:這里的 v1 和 v2 是 fn 閉包表達式內(nèi)的參數(shù),跟 exec 函數(shù)的 v1、v2 沒有任何關系
return v1 + v2
})
閉包的精簡寫法
上面 exec 的調用雖然很容易理解,但看上去有些冗長:參數(shù)類型滿天飛、fn 的參數(shù) v1、v2 跟exec本身的前兩個參數(shù)容易混淆。
強大的 Swift 編譯器允許我們對其做一些精簡,下面一步步做介紹:
-
由于在定義
exec的時候已經(jīng)明確了fn的兩個參數(shù)類型Int和返回類型Int,所以可以做如下簡化:// 只需要使用 in 關鍵字 將參數(shù)和函數(shù)體做區(qū)隔即可,省略了v1、v2的類型以及返回值 exec(v1: 10, v2: 20, fn: { v1, v2 in return v1 + v2 }) -
就跟函數(shù)一樣,閉包函數(shù)體的單行表達式可以省略
return// 省略了函數(shù)體的 return 關鍵字 exec(v1: 10, v2: 20, fn: { v1, v2 in v1 + v2 }) Swift 中可以使用
$0和$1來分別表示第0個參數(shù)和第1個參數(shù)
exec(v1: 10, v2: 20, fn: {
$0 + $1
})
- 甚至于你可以省略
$0和$1(這種方式過分了,不推薦??)
// 直接使用一個 + ,表示第0個參數(shù)和第1個參數(shù)直接是加號運算符
exec(v1: 10, v2: 20, fn: +)
尾隨閉包
如果你需要將一個很長的閉包表達式作為最后一個參數(shù)傳遞給函數(shù),將這個閉包替換成為尾隨閉包的形式很有用。
尾隨閉包是一個書寫在函數(shù)圓括號之后的閉包表達式,函數(shù)支持將其作為最后一個參數(shù)調用。
在使用尾隨閉包時,你不用寫出它的參數(shù)標簽。
還用之前的 exec 函數(shù)舉例
- 不使用尾隨閉包:
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
return v1 + v2
})
- 使用尾隨閉包:
exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in return v1 + v2 }
尾隨閉包不僅僅省略了 fn 形參,而且將{函數(shù)體}挪到了()外面讓整個函數(shù)調用更加的直觀易讀。同樣可以對其進行精簡:
swift // 省略閉包參數(shù)類型和返回值 exec(v1: 10, v2: 20) { v1, v2 in // 省略 return v1 + v2 }
然后:
swift exec(v1: 10, v2: 20) { // 直接使用 $0、$1 表示第0和第1個參數(shù) $0 + $1 }
這個表達式就非常的簡潔優(yōu)雅了!???? 注意,下面的表達式是不允許的:
swift // 尾隨閉包不允許省略$0、$1 exec(v1: 10, v2: 20) { + }
閉包的值捕獲
閉包可以理解為函數(shù)以及其捕獲的上下文中的變量或常量的總和
看下面這個函數(shù):
func getFn() -> (Int) -> Int {
var num = 0
func plus (_ i: Int) -> Int {
num = num + i
return num
}
return plus
}
getFn函數(shù)沒有參數(shù),返回值為(Int) -> Int(一個參數(shù)為Int返回值為Int的函數(shù))。
getFn函數(shù)體內(nèi)定義了一個Int類型的變量num,又定義了一個plus函數(shù),并將其作為getFn函數(shù)的返回值返回。
plus函數(shù)對num變量進行了捕獲,構成了閉包。
思考如下代碼的輸出:
var f = getFn()
print(f(1))
print(f(2))
print(f(3))
print(f(4))
結果是哪一組?
1 1
2 or 3
3 6
4 10
正如前面提到的函數(shù)以及其捕獲的上下文中的變量或常量的總和,當調用getFn()時,返回的不僅僅是plus函數(shù)同時也包括num變量組成的閉包整體!理解這個概念非常重要,因此getFn()返回的其實就是下面的代碼片段:
var num = 0
func plus (_ i: Int) -> Int {
num = num + i
return num
}
因此f(1)``f(2)``f(3)``f(4)訪問的是同一個num,或者說同一塊變量內(nèi)存
num 初始值為 0
f(1)就等價于 num = num + 1
f(2)就等價于 num = num + 2
f(3)就等價于 num = num + 3
f(4)就等價于 num = num + 4
所以結果為:
1
3
6
10
再思考這種情況:
var f1 = getFn()
print(f1(1)) // 1
print(f1(2)) // 3
print(f1(3)) // 6
var f2 = getFn()
print(f2(1)) // 1
print(f2(2)) // 3
print(f2(3)) // 6
很顯然每創(chuàng)建一個getFn函數(shù)引用(沒錯,函數(shù)和閉包都是引用類型),Swift 都會為所捕獲的num申請一份新的堆空間內(nèi)存,來保證所有的f1訪問的都是同一塊內(nèi)存地址,所有的f2訪問的也都是同一塊內(nèi)存地址,但f1和f2訪問的num堆地址不是同一塊!
自動閉包
觀察下面這個函數(shù):
// 如果第1個參數(shù)大于0則返回之,否則返回第2個參數(shù)
func getFirstPositiveNumber(n1: Int, n2: Int) -> Int {
return n1 > 0 ? n1 : n2
}
調用getFirstPositiveNumber:
func getDoubleOfNumber(_ v: Int) -> Int {
return v * 2
}
getFirstPositiveNumber(n1: 10, n2: 20) // 10
getFirstPositiveNumber(n1: -10, n2: 20) // 20
上述函數(shù)看似很簡單,但有一個隱患可以優(yōu)化:
如果n1 > 0,那么n2是什么根本不重要了,可是編譯器還是需要花費開銷去"關心"n2。你可能會不以為然,心理嘀咕『不就一個Int,至于么?"』
那下面這個例子呢:
func getNumber() -> Double {
return Double.pi * 10.0
}
getFirstPositiveNumber(n1: 10.0, n2: getNumber()) // 10.0
既然n1已經(jīng)>0了,我們?yōu)楹芜€要去調用getDoubleOfNumber來計算n2呢?
如果getDoubleOfNumber函數(shù) 計算很復雜 、 需要去讀取本地數(shù)據(jù) 甚至 需要聯(lián)網(wǎng)抓取數(shù)據(jù) 呢?這種浪費就不能不以為然了吧。
那怎么解決呢?當時是使用閉包:
func getFirstPositiveNumber(n1: Double, n2: () -> Double) -> Double {
return n1 > 0 ? n1 : n2()
}
將n2的類型從Double改為() -> Double,調用時:
getFirstPositiveNumber(n1: 10.0, n2: {
Double.pi * 10.00
})
或者使用尾隨閉包:
getFirstPositiveNumber(n1: 10) {
Double.pi * 10.00
}
當n1 > 0時,閉包的函數(shù)體Double.pi * 10.00根本不用執(zhí)行!完美!
可是每次調用getFirstPositiveNumber都要寫閉包會很繁瑣,因此Swift標準庫提供了自動閉包的語法糖來解決這個問題,getFirstPositiveNumber函數(shù)只需要像下面這么寫:
func getFirstPositiveNumber1(n1: Double, n2: @autoclosure () -> Double) -> Double {
return n1 > 0 ? n1 : n2()
}
getFirstPositiveNumber1(n1: 10, n2: Double.pi * 2)
調用時n2會自動寫成閉包的形式!
逃逸閉包
func fn1(_ closure: (Int) -> Int) {
print(closure(10))
}
fn1函數(shù)只有一個閉包參數(shù)closure,且closure在fn1函數(shù)體內(nèi)部直接調用,這時候我們稱closure為非逃逸閉包。
如果像下面這么寫編譯器就會報錯:
var c: ((Int) -> Int)?
func fn2(_ closure: (Int) -> Int) {
c = closure
}
closure在fn2作用域外調用,即成為逃逸閉包,可以使用@escaping關鍵詞消除編譯器報錯:
func fn2(_ closure: @escaping (Int) -> Int) {
c = closure
}