一:閉包表達(dá)式的定義
在 Swfit 中,可以通過func定義一個(gè)函數(shù),也可以通過閉包表達(dá)式定義一個(gè)函數(shù).閉包表達(dá)式的格式為:
{
(參數(shù)列表) -> 返回值類型 in
方法體
}
比如說我們使用func定義一個(gè)add方法:
func add(a: Int, b: Int) -> Int{
a + b
}
print(add(a: 10, b: 20))
用閉包表達(dá)式也可以定義:
let fn = {
(a: Int , b: Int) -> Int in
a + b
}
print(fn(10,20))
二:閉包表達(dá)式的簡寫
定義一個(gè)函數(shù)如下:
// 接受兩個(gè) Int 類型的參數(shù) , 和一個(gè)函數(shù)類型的參數(shù)
func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
print(fn(a,b))
}
使用閉包表達(dá)式完整調(diào)用如下:
exec(a: 10, b: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
另外還有4中簡寫方式:
- 省略參數(shù)類型 和 返回值類型
因?yàn)殚]包表達(dá)式已經(jīng)明確定義了參數(shù)類型和返回值類型都是 Int 類型,所以在調(diào)用閉包表達(dá)式的時(shí)候,編譯器能自動(dòng)類型推斷,知道具體的類型.所以類型可以省略
exec(a: 10, b: 20) {
v1, v2 in
return v1 + v2
}
- 省略 return
因?yàn)楹瘮?shù)體代碼是一個(gè)單一表達(dá)式,所以我們可以省略 return
exec(a: 10, b: 20) {
v1, v2 in v1 + v2
}
- 省略參數(shù),
$0$1分別代表第一個(gè),第二個(gè)參數(shù)
exec(a: 10, b: 20) {
$0 + $1
}
- 直接 +
如果直接寫一個(gè)運(yùn)算符,編譯器也知道是拿兩個(gè)參數(shù)直接參與運(yùn)算
exec(a: 10, b: 20, fn: +)
三:尾隨閉包
如果一個(gè)函數(shù)的最后一個(gè)實(shí)參是一個(gè)閉包表達(dá)式,并且這個(gè)閉包表達(dá)式的函數(shù)體代碼很長,為了增強(qiáng)函數(shù)的可讀性,這個(gè)閉包表達(dá)式可以采用尾隨閉包.
尾隨閉包:尾隨閉包是一個(gè)書寫在函數(shù)調(diào)用括號外面的閉包表達(dá)式.
像上面的exec函數(shù),它的最后一個(gè)參數(shù)是一個(gè)閉包表達(dá)式,那我們我們可以使用尾隨閉包來增強(qiáng)可讀性:(事實(shí)上,編譯器自動(dòng)敲出來的就是尾隨閉包)
func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
print(fn(a,b))
}
// 尾隨閉包
exec(a: 10, b: 20) { (v1, v2) -> Int in
return v1 + v2
}
如果函數(shù)只有一個(gè)參數(shù),而且這個(gè)參數(shù)是個(gè)閉包表達(dá)式,并且使用了尾隨閉包的寫法,那么在調(diào)用的時(shí)候可以省略小括號():
func add(fn: (Int, Int) -> Int){
print(fn(20,10))
}
//正常寫法
add(fn: {$0 - $1})
//尾隨閉包寫法
add(){$0 - $1}
//省略小括號()
add{$0 - $1}
示例:
Swift 中的Array有一個(gè)排序方法:
func sort(by areInIncreasingOrder: (Self.Element, Self.Element) throws -> Bool)
接收兩個(gè)參數(shù),返回一個(gè)布爾值.假設(shè)第一個(gè)參數(shù)是v1,第二個(gè)參數(shù)是v2,它的意思是:如果返回true,v1排在v2前面;如果返回false,v1排在v2后面.
常規(guī)寫法是傳入一個(gè)函數(shù):
func arraySort(){
//排序規(guī)則函數(shù)
func comp(v1: Int, v2: Int) -> Bool{
//從大到小排序
v1 > v2
}
var array = [4,2,10,19,13,8,22]
//傳入 comp 函數(shù)
array.sort(by: comp(v1:v2:))
print(array)
}
也可以使用尾隨閉包:
//閉包表達(dá)式示例 數(shù)組排序
func arraySort(){
var array = [4,2,10,19,13,8,22]
//閉包表達(dá)式常規(guī)寫法
array.sort(by: {
(v1: Int, v2: Int) -> Bool in
return v1 > v2
})
//因?yàn)槭亲詈笠粋€(gè)參數(shù),可以采用尾隨閉包寫法
array.sort(){
v1,v2 in v1 > v2
}
//因?yàn)槭亲詈笠粋€(gè)參數(shù),并且是唯一一個(gè)參數(shù),可以省略小括號
array.sort{
v1,v2 in v1 > v2
}
//使用 $ 替代參數(shù)
array.sort{
$0 > $1
}
//直接使用 >
array.sort(by: >)
print(array)
}
四:閉包
閉包和閉包表達(dá)式完全是兩個(gè)概念:
閉包表達(dá)式: 閉包表達(dá)式是定義函數(shù)的一種方式;-
閉包: 閉包是一個(gè)函數(shù)和它所捕獲的變量/常量環(huán)境組合起來,稱作閉包.
一般指定義在函數(shù)內(nèi)部的函數(shù)
一般它捕獲的是外部函數(shù)的局部變量/常量
思考一下以下代碼的打印結(jié)果是什么:
typealias Fn = (Int) -> Int
func getFn() -> Fn{
var num = 0
func sum(_ v1: Int) -> Int{
num += v1
return num
}
return sum
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
結(jié)果如下:

怎么會(huì)這樣呢?num是在getFn函數(shù)內(nèi)部定義的局部變量,在 149 行調(diào)用完getFn()后,num的內(nèi)存應(yīng)該被回收了,為什么打印的結(jié)果都是在同一塊內(nèi)存上累加的呢?
其實(shí)這就是閉包,sum 函數(shù)捕獲了 num 變量,并且把 num 變量存在了堆空間,所以即使函數(shù)調(diào)用完畢, num 變量依然沒有銷毀.
我們來看一下它的匯編代碼:

然后我們再修改一下sum函數(shù)的代碼,使其不訪問外部變量,再看看它的匯編,對比一下:

匯編如下:

可以看到,當(dāng)sum函數(shù)沒有訪問外部變量的時(shí)候,它的匯編代碼很簡單.也沒有調(diào)用allocObject向堆空間申請內(nèi)存.并且getFn返回的直接就是sum函數(shù)的地址.
現(xiàn)在,我們大概也知道了,當(dāng) sum 函數(shù)訪問了外層函數(shù)的局部變量 num 時(shí),會(huì)調(diào)用 allocObject 函數(shù)向堆空間申請內(nèi)存,把 num 變量存儲(chǔ)在堆空間,保住 num 變量的命.也就是所謂的 捕獲.
下面我們將通過匯編代碼驗(yàn)證我們的結(jié)論.
如果所示,打兩個(gè)斷點(diǎn):

運(yùn)行代碼:

上面沒有訪問外部變量時(shí),getFn直接返回的就是sum函數(shù)的地址.但是這里不一樣了,因?yàn)樵L問了外部變量,getFn返回的是一個(gè)堆空間的地址.這個(gè)地址中有可以找到sum函數(shù)地址的線索,并且還要存儲(chǔ)捕獲的num變量.
繼續(xù)跳到下一個(gè)斷點(diǎn):

執(zhí)行完fn(1),內(nèi)存中的值變成了 1.
繼續(xù)跳過斷點(diǎn),會(huì)執(zhí)行fn(2):

執(zhí)行fn(3):

執(zhí)行fn(4):

通過上面的分析,已經(jīng)很清晰的知道了當(dāng)函數(shù)訪問了外部函數(shù)的局部變量后,會(huì)向堆空間申請內(nèi)存,用來捕獲外部函數(shù)的變量.
思考一下,調(diào)用兩次getFn()下面代碼打印結(jié)果是什么?

我們看看它的匯編代碼:

返回了兩個(gè)Fn對象:

執(zhí)行 fn1(1):

執(zhí)行 fn2(2):

執(zhí)行 fn1(3):

執(zhí)行 fn2(4):

可以看到,執(zhí)行多少次getFn(),就會(huì)創(chuàng)建多少個(gè)Fn對象,并且每個(gè)對象之間互不影響.他們都有自己的內(nèi)存空間,都有自己的捕獲對象.
上面的fn1 , fn2和它捕獲的變量組合在一起就是一個(gè)閉包,其實(shí)閉包和類的實(shí)例對象很相似,我們通過兩張圖對比一下:

另外他們都存儲(chǔ)在堆空間,并且內(nèi)存布局也一樣,所以我們可以把閉包想象成一個(gè)實(shí)例對象,這樣更容易理解.