1休傍、閉包表達(dá)式與閉包
閉包表達(dá)式也就是定義一個函數(shù)。
一般我們可以通過func
定義一個函數(shù)卷中,也可以通過閉包表達(dá)式定義一個函數(shù)氓涣。
閉包與閉包表達(dá)式的區(qū)別是閉包會捕獲外部變量/常量,但有時候會將閉包表達(dá)式也稱為閉包达传,但實際上只是簡稱篙耗。
func sum(v1: Int, v2: Int) -> Int {
v1 + v2
}
進(jìn)一步寫成閉包表達(dá)式:
var sum = {
(v1: Int, v2: Int) -> Int in
v1 + v2
}
可以看到上面的函數(shù)與閉包表達(dá)式的作用是一樣的。
而閉包表達(dá)式也是可以作為函數(shù)的參數(shù)
func test(v1: Int, v2: Int, fn: (Int, Int) -> Int) -> Int {
return fn(v1, v2)
}
test(v1: 1, v2: 2, fn: sum)
也可以這樣寫宪赶,也就是直接傳入閉包表達(dá)式:
test(v1: 1, v2: 2, fn: {
(v1: Int, v2: Int) -> Int in
v1 + v2
})
可以更簡化一步
test(v1: 1, v2: 2, fn: {
v1, v2 in
v1 + v2
})
再來簡化一下:
test(v1: 1, v2: 2, fn: {
$0 + $1
})
甚至于直接傳入一個+號
test(v1: 1, v2: 2, fn: +)
尾隨閉包
如果將閉包表達(dá)式作為函數(shù)的最后一個實參宗弯,使用尾隨閉包可以增強函數(shù)的可讀性。
因此在我們實際寫代碼的時候編譯器有時候會提示將最后的閉包表達(dá)式修改為尾隨閉包的形式搂妻。
上面所述的例子既可以寫成尾隨閉包的形式
test(v1: 1, v2: 3) {
v1, v2 in
v1 + v2
}
test(v1: 1, v2: 3) {
$0 + $1
}
如果閉包表達(dá)式是函數(shù)的唯一實參蒙保,而且使用了尾隨閉包的語法,那么不需要在函數(shù)后寫圓括號欲主。
2邓厕、閉包
一個函數(shù)和它捕獲的變量常量環(huán)境組合起來稱為閉包
一般情況下這個函數(shù)是定義在函數(shù)內(nèi)部的,也就是這個內(nèi)部的函數(shù)與這個包裹其的函數(shù)內(nèi)部定義的變量一起稱之為閉包扁瓢。
如下面的例子plus
以及其捕獲的變量a
一起才是閉包详恼。
下面我們先看一下代碼,可以看到這里內(nèi)部的函數(shù)捕獲變量a
:
typealias Fn = (Int) -> (Int)
var a = 10
func exec() -> Fn {
func plus (_ i: Int) -> Int {
a += i
return a
}
return plus
}
let fn = exec()
print(fn(1))
print(fn(2))
此時可以想一下打印的結(jié)果是多少
11
13
因為這里的a
是一個全局變量引几,因此我們調(diào)用了兩次訪問的都是同一個變量地址昧互,因此這里的結(jié)果顯而易見了。
那如果a是一個局部變量呢伟桅?
func exec() -> Fn {
var a = 10
func plus (_ i: Int) -> Int {
a += i
return a
}
return plus
}
此時我們再去調(diào)用的結(jié)果是什么敞掘?
實際上這里的結(jié)果與上面一致,按道理我們在執(zhí)行完exec
函數(shù)獲得返回值fn
之后其內(nèi)部的變量a
應(yīng)該是會被釋放掉的楣铁。
此時我們添加斷點到 return plus
然后查看匯編代碼玖雁。
此時我們可以看到在第9行處調(diào)用了一個函數(shù),后邊有描述為
swift_allocObject
民褂,這說明了為其在堆空間分配了內(nèi)存茄菊。而實際上我們的變量都是局部變量,按道理應(yīng)該是在棧上使用赊堪,使用完后就會被銷毀釋放掉面殖。也就是我們在返回plus
時,捕獲了外部變量哭廉,但是這個變量是局部變量脊僚,因為為了延長變量a
的生命周期因為將其放入了堆空間。那么之前打印結(jié)果的問題也就解釋了,我們調(diào)用一次
exec
辽幌,此時就會分配一次堆空間增淹,堆空間內(nèi)存放著的就是變量的值。因此我們每次調(diào)用fn
的時候使用了同一個變量乌企。那么我們再調(diào)用
exec
獲取一個函數(shù)
let fn = exec()
print(fn(1))
print(fn(2))
let fn1 = exec()
print(fn1(3))
print(fn1(4))
此時的打印結(jié)果也就知道虑润,這里是分開的兩個堆空間。
那么我們來看一下當(dāng)前的堆空間中是否是存放著局部變量加酵。
此時也在return a
處添加上斷點拳喻,執(zhí)行然后進(jìn)入到匯編代碼,
因為我們在分配堆空間處也有斷點
因此我們著這個斷點處查看一下當(dāng)前堆空間的地址猪腕。之前我們也提到過call
函數(shù)后的返回值一般都是在rax
中冗澈,所以這里我們只需要讀取一下rax
中的值即可。
(lldb) register read rax
rax = 0x000000010055f0e0
實際上這個地址存放的也就是變量a
的值陋葡,但是這塊地址是剛剛分配的亚亲,也就是這里面還沒有數(shù)據(jù)或者有的也是垃圾數(shù)據(jù)。
此時我們進(jìn)入到上面提到過的return a
處的斷點腐缤,根據(jù)代碼我們是return
了一個變量捌归,那么也就是這個變量實際上已經(jīng)是有值的,此時我們查看上面分配的堆空間里邊的數(shù)據(jù)
(lldb) x/5xg 0x000000010055f0e0
0x10055f0e0: 0x0000000100008158 0x0000000200000002
0x10055f0f0: 0x000000000000000b 0x0000000000000000
0x10055f100: 0x0000000000000000
前16個字節(jié)不用管柴梆,直接從第17個字節(jié)開始看陨溅,可以看到我們第一次執(zhí)行fn(1)
的結(jié)果存放在里邊终惑。所以這里也就印證了前面所說的分配的堆空間存放的正是變量a
的值绍在。
同時我們也可以返回再看一下exec()
的匯編截圖中的第13
行處movq $0xa, 0x10(%rax)
,這是是有個move
執(zhí)行操作雹有,上述我們知道rax
中存放著變量的堆空間地址偿渡,這里是直接將值賦值到了這個地址的第3個8個字節(jié),也就是跳過了前面的16個字節(jié)霸奕。也就是直接將這個變量從椓锟恚空間拷貝到了堆空間。
那么這里分配了多大的堆空間呢质帅。我們再回到exec
匯編的截圖處适揉,此時在swift_allocObjec
之前我們可以看到對應(yīng)有3個參數(shù)傳遞rdi
、rsi
煤惩、rdx
嫉嘀,但是我們再看一其前面?zhèn)鬟f的數(shù)據(jù)大概可以知道 rsi
里邊才是存放的需要的空間大小為24
,但是因為內(nèi)存對齊的緣故魄揉,所以應(yīng)該是分配了32
個字節(jié)剪侮。
rax、rdx常作為函數(shù)返回值使用
rdi洛退、rsi瓣俯、rdx杰标、rcx、r8彩匕、r9等寄存器常用于存放函數(shù)參數(shù)
rsp腔剂、rbp用于棧操作
我們可以通過實例化類的方式來確認(rèn)一下上面所說的。
定義一個類驼仪,并實例化一個對象
進(jìn)入?yún)R編桶蝎,在
SwiftStudy.Person.__allocating_init()
處打上斷點進(jìn)入到
allocating_init
可以看到這里也是即將分配堆空間
swift_allocObject
,此時我們看第6行谅畅,通過rsi
傳參時登渣,這里傳遞了32
個字節(jié),前16
個字節(jié)固定毡泻,后16
為內(nèi)部變量胜茧。
其實我們可以將閉包看做是一個類對象,上面定義的exec
可以看做是一個類對象仇味,其內(nèi)部函數(shù)捕獲的變量a
就是其成員變量呻顽,而函數(shù)就是其內(nèi)部的函數(shù)。并且類對象就是分配在堆空間上的丹墨。甚至其在堆空間上的數(shù)據(jù)分配也是和類對象基本一致的廊遍,類的前16
個字節(jié)存放類信息以及引用計數(shù),同樣閉包的堆空間的前16
個字節(jié)也是存放著一些信息贩挣,真正的數(shù)據(jù)是從第17
個字節(jié)開始喉前。
問題:前面我們分析的是局部變量,那如果是全局變量此時還會在堆是哪個分配對應(yīng)的空間嗎王财?