作者:Olivier Halligon,原文鏈接,原文日期:2016-07-25
譯者:walkingway;校對(duì):小鍋;定稿:CMB
盡管現(xiàn)在已經(jīng)是 ARC 的天下了,但對(duì)于程序員來說理解內(nèi)存管理和對(duì)象的生命周期依然是一門必修課。對(duì)于在 Swift 當(dāng)中廣泛應(yīng)用的閉包就是其中一個(gè)特殊的例子,與 Objc 的閉包相比,Swift 的閉包也有著不同的捕獲語義。下面讓我們看看閉包是如何工作的。
介紹
在 Swift 中,閉包捕獲他們所引用的變量:雖然這些變量在閉包之外聲明,但只要在閉包內(nèi)使用都會(huì)默認(rèn)被閉包保留引用(retain),這是為了確保閉包執(zhí)行時(shí),這些變量還活著(譯者注:沒有被提前釋放)。
在文章接下來的部分,我們來定義一個(gè)簡單的 Pokemon(口袋妖怪)類:
class Pokemon: CustomDebugStringConvertible {
let name: String
init(name: String) {
self.name = name
}
var debugDescription: String { return "<Pokemon \(name)>" }
deinit { print("\(self) escaped!") }
}
接下來聲明一個(gè)簡單的函數(shù),他接受一個(gè)閉包作為參數(shù),然后在一段時(shí)間后執(zhí)行這個(gè)閉包(使用 GCD)。下面的例子展示了閉包是如何捕獲外部變量的。
func delay(seconds: NSTimeInterval, closure: ()->()) {
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue()) {
print("??")
closure()
}
}
在 Swift 3 中,上面的函數(shù)應(yīng)該變成下面這種形式:
func delay(seconds: Int, closure: ()->()) {
let time = DispatchTime.now() + .seconds(seconds)
DispatchQueue.main.after(when: time) {
print("??")
closure()
}
}
默認(rèn)的捕獲語義
現(xiàn)在,先從一個(gè)簡單的例子開始:
func demo1() {
let pokemon = Pokemon(name: "Mewtwo")
print("before closure: \(pokemon)")
delay(1) {
print("inside closure: \(pokemon)")
}
print("bye")
}
這個(gè)例子看上去很簡單,但它有趣的地方在于閉包的運(yùn)行被推遲了 1 秒鐘,所以當(dāng) demo1() 函數(shù)執(zhí)行完畢后,閉包才開始執(zhí)行;并且 1 秒后當(dāng)閉包被執(zhí)行的時(shí)候 Pokemon 實(shí)例依然存活著。
before closure: <Pokemon Mewtwo>
bye
??
inside closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!
這是因?yàn)殚]包捕獲(強(qiáng)引用)了 pokemon 變量:編譯器發(fā)現(xiàn)在閉包內(nèi)部引用了 pokemon 變量,它會(huì)自動(dòng)捕獲該變量(默認(rèn)是強(qiáng)引用),所以 pokemon 的生命周期與閉包自身是一致的。
因此,閉包有點(diǎn)像精靈球 ??,只要你持有著精靈球閉包,pokemon 變量也就會(huì)在那里,不過一旦精靈球閉包被釋放,引用的 pokemon 也會(huì)被釋放。
在這個(gè)例子中,一旦 GCD 執(zhí)行完畢,閉包就會(huì)被釋放,所以 pokemon 的 deinit 方法也會(huì)被調(diào)用。
如果 Swift 沒有自動(dòng)捕獲 pokemon 變量,這就意味著當(dāng)執(zhí)行到 demo1 函數(shù)結(jié)尾時(shí),pokemon 變量將會(huì)脫離作用域,隨后當(dāng)閉包執(zhí)行時(shí),pokemon 就已經(jīng)不存在了...這可能會(huì)導(dǎo)致程序崩潰。
幸虧 Swift 足夠聰明,閉包會(huì)自動(dòng)為我們捕獲pokemon。接下來我們會(huì)學(xué)習(xí)在必要時(shí)如何弱捕獲(弱引用)這些變量。
被捕獲的變量在執(zhí)行時(shí)才取值
有一點(diǎn)值得注意的是 Swift 在閉包執(zhí)行時(shí)才會(huì)取出捕獲變量的值[^1]。我們可以認(rèn)為它之前捕獲的是變量的引用(或指針)。
這里有一個(gè)有趣的例子:
func demo2() {
var pokemon = Pokemon(name: "Pikachu")
print("before closure: \(pokemon)")
delay(1) {
print("inside closure: \(pokemon)")
}
pokemon = Pokemon(name: "Mewtwo")
print("after closure: \(pokemon)")
}
你能猜猜打印的結(jié)果嗎?答案如下:
before closure: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
after closure: <Pokemon Mewtwo>
??
inside closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!
請注意我們在創(chuàng)建完閉包之后修改了 pokemon 對(duì)象,閉包延遲一秒后執(zhí)行(雖然此時(shí)已經(jīng)脫離了 demo2() 函數(shù)的作用域),我們打印的結(jié)果是新的 pokemon 對(duì)象,而不是舊的!這是因?yàn)?Swift 默認(rèn)捕獲的是變量的引用。
具體的細(xì)節(jié)為:首先初始化一個(gè)值為 Pikachu 的 pokemon 對(duì)象,接著修改該對(duì)象的值為 Mewtwo(譯者注:創(chuàng)建了新對(duì)象),之前值為 Pikachu 的對(duì)象由于沒有其他變量強(qiáng)引用,所以會(huì)被釋放。接著閉包等待一秒鐘執(zhí)行,打印捕獲 pokemon 變量(引用)的內(nèi)容。
這個(gè)特性對(duì)于值類型也是一樣的,關(guān)于這一點(diǎn)或許會(huì)有些奇怪,比如下面例子中的 Int 類型:
func demo3() {
var value = 42
print("before closure: \(value)")
delay(1) {
print("inside closure: \(value)")
}
value = 1337
print("after closure: \(value)")
}
打印結(jié)果:
before closure: 42
after closure: 1337
??
inside closure: 1337
你沒看錯(cuò),閉包打印了新的整型變量值---盡管整型變量是值類型!---因?yàn)樗东@了變量的引用,而不是變量自身的內(nèi)容!
你可以修改閉包中捕獲的變量
如果捕獲的是變量 var(而不是常量 let),你也可以在閉包中[^2]修改它的值。
func demo4() {
var value = 42
print("before closure: \(value)")
delay(1) {
print("inside closure 1, before change: \(value)")
value = 1337
print("inside closure 1, after change: \(value)")
}
delay(2) {
print("inside closure 2: \(value)")
}
}
代碼的打印結(jié)果如下:
before closure: 42
??
inside closure 1, before change: 42
inside closure 1, after change: 1337
??
inside closure 2: 1337
變量 value 的值在閉包內(nèi)部被修改了(盡管它已經(jīng)被捕獲了,但并不等同于一個(gè)常量拷貝,它依然保持著對(duì)原變量的引用)。接著第二個(gè)閉包執(zhí)行時(shí)打印的就是這個(gè)新值了,此刻第一個(gè)閉包已經(jīng)執(zhí)行完畢并釋放了所有的引用,而且 value 變量也脫離了 demo4() 函數(shù)的作用域。
捕獲一個(gè)變量作為一個(gè)常量拷貝
如果想要在閉包創(chuàng)建時(shí)捕獲變量的值,而不是在閉包執(zhí)行時(shí)才去獲取變量的值,你可以使用 捕獲列表
捕獲列表寫在閉包的方括號(hào)之間,緊跟閉包的左括號(hào)(并且在閉包的參數(shù)或返回類型之前)[^3]
在創(chuàng)建閉包時(shí)捕獲變量的值(而不是變量的引用),你可以使用 [localVar = varToCapture] 捕獲列表??瓷先ハ襁@樣:
func demo5() {
var value = 42
print("before closure: \(value)")
delay(1) { [constValue = value] in
print("inside closure: \(constValue)")
}
value = 1337
print("after closure: \(value)")
}
打印結(jié)果:
before closure: 42
after closure: 1337
??
inside closure: 42
與上面的 demo3() 比較,這次閉包打印的是變量創(chuàng)建時(shí)的值,而不是后來賦的新值 1337,即使整個(gè)閉包的執(zhí)行是在對(duì)變量重新賦值之后。
這就是 [constValue = value] 在閉包中所做的事情:在閉包創(chuàng)建時(shí)捕獲變量 value 的內(nèi)容 --- 而不是變量的引用。
回到 Pokemon 上
正如我們上面所看到的:如果一個(gè)變量是引用類型---就像我們的 Pokemon 類,閉包并沒有真正(強(qiáng)引用)捕獲變量的引用,而是捕獲了一個(gè)針對(duì)原始實(shí)例 pokemon 的拷貝:
func demo6() {
var pokemon = Pokemon(name: "Pikachu")
print("before closure: \(pokemon)")
delay(1) { [pokemonCopy = pokemon] in
print("inside closure: \(pokemonCopy)")
}
pokemon = Pokemon(name: "Mewtwo")
print("after closure: \(pokemon)")
}
這類似創(chuàng)建了一個(gè)中間變量指向同一個(gè) pokemon,然后捕獲了這個(gè)中間變量:
func demo6_equivalent() {
var pokemon = Pokemon(name: "Pikachu")
print("before closure: \(pokemon)")
// here we create an intermediate variable to hold the instance
// pointed by the variable at that point in the code:
let pokemonCopy = pokemon
delay(1) {
print("inside closure: \(pokemonCopy)")
}
pokemon = Pokemon(name: "Mewtwo")
print("after closure: \(pokemon)")
}
事實(shí)上,使用捕獲列表完全等同于上述代碼的行為...除了中間變量 pokemonCopy 屬于閉包的局部變量,只能在閉包內(nèi)部訪問。
相比 demo2() 直接使用 pokemon,demo6() 則使用了 [pokemonCopy = pokemon] in …,demo6() 輸出如下:
before closure: <Pokemon Pikachu>
after closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!
??
inside closure: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
以下是詳細(xì)過程:
- 皮卡丘(Pikachu)被創(chuàng)造。
- 接著閉包捕獲了 Pikachu 的拷貝(這里實(shí)際上是捕獲了 pokemon 變量的值)。
- 所以當(dāng)我們緊接著為 pokemon 變量賦新值 “Mewtwo” 后,“Pikachu” 還沒有被釋放,依然被閉包所保留。
- 當(dāng)我們離開
demo6函數(shù)的作用域,Mewtwo 就被釋放了,在方法內(nèi)部 pokemon 變量自身只被一個(gè)強(qiáng)引用所保持,離開作用域強(qiáng)引用也就消失了。 - 稍后閉包執(zhí)行時(shí),打印了
"Pikachu",這是因?yàn)樵陂]包創(chuàng)建時(shí),捕獲列表就捕獲了 Pokemon。 - 最后 GCD 釋放了閉包,由此可以證明閉包保持了口袋妖怪皮卡丘(Pikachu pokemon)的引用。
與此剛好相反,我們來分析下 demo2 的代碼:
- 皮卡丘(Pikachu)被創(chuàng)造。
- 閉包只捕獲了
pokemon變量的引用,而不是捕獲其所包含的值 Pickachu - 所以當(dāng) pokemon 隨后被分配了一個(gè)新值
"Mewtwo",此時(shí)沒有任何對(duì)象的強(qiáng)引用指向 Pikachu,它也會(huì)立即被釋放。 - 因此閉包稍后執(zhí)行打印的結(jié)果也是 Mewtwo
- 在閉包執(zhí)行完畢后 GCD 會(huì)釋放閉包,此時(shí) Mewtwo pokemon 會(huì)隨閉包一起被釋放。
知識(shí)點(diǎn)整合
上面的知識(shí)點(diǎn)都掌握了嗎?我承認(rèn),確實(shí)有點(diǎn)多...
下面是一個(gè)人為設(shè)計(jì)的例子,它包含了閉包創(chuàng)建時(shí)就對(duì)變量取值---歸功于捕獲列表,以及先捕獲變量的引用,而真正的取值放到閉包執(zhí)行時(shí)這兩種情形:
func demo7() {
var pokemon = Pokemon(name: "Mew")
print("?? Initial pokemon is \(pokemon)")
delay(1) { [capturedPokemon = pokemon] in
print("closure 1 — pokemon captured at creation time: \(capturedPokemon)")
print("closure 1 — variable evaluated at execution time: \(pokemon)")
pokemon = Pokemon(name: "Pikachu")
print("closure 1 - pokemon has been now set to \(pokemon)")
}
pokemon = Pokemon(name: "Mewtwo")
print("?? pokemon changed to \(pokemon)")
delay(2) { [capturedPokemon = pokemon] in
print("closure 2 — pokemon captured at creation time: \(capturedPokemon)")
print("closure 2 — variable evaluated at execution time: \(pokemon)")
pokemon = Pokemon(name: "Charizard")
print("closure 2 - value has been now set to \(pokemon)")
}
}
能猜猜打印結(jié)果是什么嗎?可能有點(diǎn)難猜,不過這是一個(gè)很好的練習(xí),通過自己判斷打印結(jié)果來測試你是否掌握了今天的課程...

下面是打印結(jié)果,你猜對(duì)了嗎?
?? Initial pokemon is <Pokemon Mew>
?? pokemon changed to <Pokemon Mewtwo>
??
closure 1 — pokemon captured at creation time: <Pokemon Mew>
closure 1 — variable evaluated at execution time: <Pokemon Mewtwo>
closure 1 - pokemon has been now set to <Pokemon Pikachu>
<Pokemon Mew> escaped!
??
closure 2 — pokemon captured at creation time: <Pokemon Mewtwo>
closure 2 — variable evaluated at execution time: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
closure 2 - value has been now set to <Pokemon Charizard>
<Pokemon Mewtwo> escaped!
<Pokemon Charizard> escaped!
所以,到底發(fā)生了什么?稍微有點(diǎn)復(fù)雜,讓我給大家來逐步解釋一下:
- 把 ??
pokemon一開始設(shè)置為Mew - 創(chuàng)建閉包 1 并且它的新本地變量
capturedPokemon捕獲了pokemon的值(此刻pokemon的值為New,并且閉包也捕獲了pokemon變量的引用,capturedPokemon和pokemeon都會(huì)在閉包代碼中使用) - ?? 然后將
pokemon修改為Mewtwo - 創(chuàng)建閉包 2,它的新本地變量
capturedPokemon捕獲了pokemon的值(此刻pokemon的值為Mewtwo,并且閉包也捕獲了pokemon變量的引用,capturedPokemon和pokemeon都會(huì)在閉包代碼中使用) - 此刻,
demo7()函數(shù)已經(jīng)執(zhí)行完畢了 - 一秒鐘后,GCD 執(zhí)行第一個(gè)閉包
- 它的打印結(jié)果為
Mew,即第二步創(chuàng)建閉包時(shí)捕獲在capturedPokemon變量中的值 - 它也會(huì)根據(jù)所捕獲
pokemon的引用,找出變量的當(dāng)前值,它目前為Mewtwo(至少是在第五步離開demo7()函數(shù)前的值) - 然后將變量
pokemon的值改為Pikachu(再次強(qiáng)調(diào),閉包捕獲的是變量pokemon的引用,所以 demo7() 函數(shù)中的pokemon變量與閉包中進(jìn)行賦值操作的pokemon變量具有同的引用) - 當(dāng)閉包執(zhí)行完畢被 GCD 釋放后,沒有對(duì)象在強(qiáng)引用
Mew了,因此會(huì)釋放掉。但是第二個(gè)閉包的capturedPokemon依然捕獲著Mewtwo,并且第二個(gè)閉包也捕獲了pokemon變量的引用,此刻它的值為Pikachu
- ?? 又過了一秒鐘,GCD 開始執(zhí)行第二個(gè)閉包
- 它的打印結(jié)果為
Mewtwo,即步驟四第二個(gè)閉包創(chuàng)建時(shí)捕獲在capturedPokemon變量中的值 - 它也會(huì)根據(jù)所捕獲
pokemon的引用,找出變量的當(dāng)前值,它目前為Pikachu(因?yàn)樵诘谝粋€(gè)閉包中已經(jīng)修改了它) - 最后,將
pokemon變量設(shè)置為Charizard,由于 Pikachu 小精靈只被pokemon變量強(qiáng)引用,而此時(shí)pokemon已不再指向它了,所以也會(huì)立即被釋放。 - 當(dāng)閉包執(zhí)行完畢被 GCD 釋放后,本地變量
capturedPokemon脫離了作用域,所以Mewtwo會(huì)被釋放,同時(shí)指向pokemon變量的強(qiáng)引用也會(huì)消失,小精靈Charizard也會(huì)被釋放
總結(jié)
是不是感覺有點(diǎn)燒腦?這很正常,閉包捕獲語義有時(shí)候會(huì)比較復(fù)雜,尤其類似最后那個(gè)例子。我們要記住下面幾個(gè)關(guān)鍵點(diǎn):
- 在 Swift 閉包中使用的所有外部變量,閉包會(huì)自動(dòng)捕獲這些變量的引用
- 在閉包執(zhí)行時(shí),會(huì)根據(jù)這些變量引用得到所對(duì)應(yīng)的具體值
- 因?yàn)槲覀儾东@的是變量的引用(而不是變量自身的值),所以你可以在閉包內(nèi)部修改變量的值(當(dāng)然變量要聲明為
var,而不能是let) - 你可以在閉包創(chuàng)建時(shí)獲取變量中的值,然后把它存儲(chǔ)到本地常量中,而不是捕獲變量的引用。我們可以使用帶中括號(hào)的捕獲列表來實(shí)現(xiàn)。
今天的課程就先學(xué)到這里,或許有些難以理解。不要猶豫,打開你的 Playground 嘗試測試、修改、運(yùn)行這些代碼,直到你徹底理解了其中的原理。
一旦你理解了以上內(nèi)容,就可以期待我的下一篇文章了,接下來我會(huì)討論捕獲弱變量(weakly)來避免循環(huán)引用,以及閉包中的 [weak self] 和 [unowned self] 意味著什么。
感謝 @merowing 和我在 Slack 上針對(duì)所有的閉包語義所做的討論,包括在閉包執(zhí)行時(shí)才對(duì)捕獲變量取值的事實(shí)。大家感興趣的話,可以訪問他的 blog ??
對(duì)于熟悉 Objective-C 的同學(xué)已經(jīng)注意到 Swift 的行為和 Objective-C 的默認(rèn)閉包語義不同,而是有些類似于 Objective-C 中的變量帶一個(gè) __block 修飾符。
與 ObjC 的默認(rèn)行為不同...更像是在 Objective-C 中使用
__block。注意即使在我們的例子中僅捕獲了一個(gè)變量,在捕獲列表中你可以列出不止一個(gè)捕獲的變量,這就是為什么稱它為列表(lists)的原因。并且即使沒有顯式地寫出閉包參數(shù)列表,你依然要將
in關(guān)鍵字放置于捕獲列表的后面,和閉包正文分隔開來。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg。