閉包捕獲語義第一彈:一網(wǎng)打盡!

作者: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ì)被釋放,所以 pokemondeinit 方法也會(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ù)雜,讓我給大家來逐步解釋一下:

  1. 把 ?? pokemon 一開始設(shè)置為 Mew
  2. 創(chuàng)建閉包 1 并且它的新本地變量 capturedPokemon 捕獲了 pokemon 的值(此刻 pokemon 的值為 New,并且閉包也捕獲了 pokemon 變量的引用,capturedPokemonpokemeon 都會(huì)在閉包代碼中使用)
  3. ?? 然后將 pokemon 修改為 Mewtwo
  4. 創(chuàng)建閉包 2,它的新本地變量 capturedPokemon 捕獲了 pokemon 的值(此刻 pokemon 的值為 Mewtwo,并且閉包也捕獲了 pokemon 變量的引用,capturedPokemonpokemeon 都會(huì)在閉包代碼中使用)
  5. 此刻,demo7() 函數(shù)已經(jīng)執(zhí)行完畢了
  6. 一秒鐘后,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
  1. ?? 又過了一秒鐘,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 ??

  1. 對(duì)于熟悉 Objective-C 的同學(xué)已經(jīng)注意到 Swift 的行為和 Objective-C 的默認(rèn)閉包語義不同,而是有些類似于 Objective-C 中的變量帶一個(gè) __block 修飾符。

  2. 與 ObjC 的默認(rèn)行為不同...更像是在 Objective-C 中使用 __block。

  3. 注意即使在我們的例子中僅捕獲了一個(gè)變量,在捕獲列表中你可以列出不止一個(gè)捕獲的變量,這就是為什么稱它為列表(lists)的原因。并且即使沒有顯式地寫出閉包參數(shù)列表,你依然要將 in 關(guān)鍵字放置于捕獲列表的后面,和閉包正文分隔開來。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容