<b>本文講的是深入了解 Swift 中閉包的捕捉語義(一),</b>
<b></b>
即使是有 <code>ARC</code> 的今天,了解記憶體管理和對象的生命周期仍舊是非常重要的。當使用閉包的時候是一個特例,它在 <code>Swift</code> 中出現的場景越來越多,比起 <code>Objective</code> 的代碼塊的捕獲規則有很多不同的捕獲文法。讓我們看看它們是如果工作的吧。
在 <code>Swift</code> 中,閉包捕獲了他們引用到的變量: 預設情況下,在閉包外申明的變量會被使用這些變量的閉包在内部保留,為了確定他們在閉包被執行的時候仍舊存在。
對于這篇文章的來說,讓我們定義一個簡單的 <code>Pokemon</code> 類,舉個例子:
讓我們聲明一個簡單的方法,它用閉包作為參數,并且過幾秒後(使用 <code>GCD</code>)執行這個閉包。通過這個方法,我們用下面的這個例子來看看閉包是如何捕捉外部變量的。
️ 在 <code>Swift 3</code> 中,上面的方法将會被這樣的形式替換改寫:
現在,讓我們開始一個簡單的例子:
這看上去很簡單,但是有趣的是,這個閉包會在 <code>demo1()</code> 方法函數執行完成後 1 秒後被執行,并且我們已退出了方法函數的作用域... 當然 <code>Pokemon</code> 仍然是存在的,當這個代碼塊在下一個 1 秒後再次被執行的時候!
這是因為這個閉包堅定地捕獲了這個 <code>pokemon</code> 變量: 因為 <code>Swfit</code> 的編譯器看見了這個被閉包内部引用的 <code>pokemon</code> 變量,它便自動的捕獲了這個(預設情況下強捕獲),是以這個 <code>pokemon</code> 是會一直存在的,隻要這個閉包也存在。
是以,閉包很像 <code>精靈球</code>
隻要你保留精靈球在閉包周圍, <code>pokemon</code> 變量也會同樣在這裡,但是當那個精靈球被釋放了,那個被引用的 <code>pokemon</code> 變量也會被釋放。
在這個例子中,當這個閉包被 <code>GCD</code> 執行後,這個閉包自行釋放,就是 <code>Pokemon</code> 内部的 <code>init</code> 方法執行的時候。
如果 <code>Swift</code> 并沒有自動捕獲到這個 <code>pokemon</code> 變量,這意味着這個 <code>pokemon</code> 必将有時間跳出這個作用域,當調用到<code>demo1</code> 方法的尾端的時候,并且當這個閉包被下一個後 1 秒再次執行的時候,這個 <code>pokemon</code> 将不會再存在... 可能會導緻一個崩潰。
謝天謝地,<code>Swift</code> 聰明多了,并且它能為我們捕獲到這個 <code>pokemon</code>。在之後的文章裡,我們能看到,當我們需要他們的時候,怎麼去弱捕獲這些變量。
是以,這裡有一個有趣的例子:
你能猜到什麼會被列印出來麼?這裡是答案:
注意,在建立了閉包_之後_,我們改變了 <code>pokemon</code> 對象,當這個閉包在 1 秒之後執行(當我們已經從 <code>demo2()</code> 函數方法作用域退出了),我們列印出了一個新的 <code>pokemon</code>,并不是先前舊的那個!這是因為,<code>Swift</code> 預設捕獲到了變量的引用。
是以在這裡,我們把 <code>pokemon</code> 初始化成 <code>Pikachu</code>,之後,我們把它的值改成 <code>Mewtwo</code>,是以 <code>Pikachu</code> (的引用)被釋放了 - 因為再沒有其他變量保留它了。1 秒鐘之後,這個閉包被執行,并且它列印出了變量 <code>pokemon</code> 的内容,它是由閉包通過引用捕獲的。
這個閉包并沒有捕獲 <code>Pikachu</code>(這個 <code>pokemon</code> 是在閉包建立的時候我們獲得的),但更是對 <code>pokemon</code> 變量的引用 - 當這個閉包被執行的時候,它現在被定值為<code>Mewtwo</code>。
令人奇怪的是,這個在<code>值類型</code>中也行得通,例如 <code>Int</code>:
結果是:
是的,這個閉包列印出了_新_的 <code>Int</code> 的值 - 即使 <code>Int</code> 是一個<code>值類型</code>! - 因為它捕獲了變量的引用,不是變量本身的内容。
這個代碼運作的結果是:
是以在這裡,這個 <code>value</code> 變量已經從代碼塊的内部被改變了(即使他被捕獲了,他也并不是以一個靜态拷貝捕獲的,但是仍然引用了同一個變量)。并且第二個代碼塊看到新的值,即使它在之後被執行 - 并且當第一個代碼塊已經被釋放的時候,它已經離開 <code>demo4()</code> 方法函數的作用域了!
如果你想要在閉包建立的時候捕獲變量的值,而不是僅僅當閉包執行的時候去擷取它的定值,你能使用一個捕獲清單。
為了在閉包建立的時候,捕獲變量的值(而不是這個變量本身的引用),你可以使用 <code>[localVar = varToCapture]</code> 捕獲清單。以下是它大概的樣子:
結果會是:
與之前的 <code>demo3()</code> 的代碼對比,(我們會)發現這個值可以被閉包列印出了... 是 <code>value</code> 變量的内容,在閉包被建立的時候 - 在它被指派為新的 <code>1337</code> 之前 - 即使這個代碼塊在這個新的指派_之後_被執行。
這就是 <code>[constValue = value]</code> 在閉包裡的作用: 當閉包被建立的時候,捕獲 <code>value</code> 的_值_ - 并且不是這個變量本身被定值之後的引用。
我們在上面看到的,也意味着,如果這個值是一個引用類型 - 就好像我們的 <code>Pokemon</code> 類 - 這個閉包并沒有強捕獲這個變量的引用,而是捕獲到了一個原始執行個體的副本,在被捕獲的時候,包含在 <code>pokemon</code> 變量中的。
這就好像,如果我們建立一個中間變量去指向同一個 <code>pokemon</code>,并且捕獲這個變量:
事實上,使用這個捕獲清單和上面的代碼一樣... 除了這個 <code>pokemonCopy</code> 的中間變量是閉包的局部變量,并且将隻能在閉包内被通路。
和這個 <code>demo6()</code> 對比 - 它使用 <code>[pokemonCopy = pokemon] in ...</code> - 而且 <code>demo2()</code> - 它并沒有,相反直接使用<code>pokemon</code>。<code>demo6()</code> 輸出了這個:
以下解釋了發生了什麼:
<code>Pikachu</code> 被建立了;
之後它通過閉包被以一個副本形式捕獲(捕獲了 <code>pokemon</code> 的值)
是以,在後面的幾行代碼中,我們為 <code>pokemon</code> 賦上一個新的值 <code>Pokemon Mewtwo</code>,此時 <code>Pikachu</code> _恰好_沒有被釋放,因為它仍被閉包保留着。
當我們從 <code>demo6</code> 方法函數作用域中退出,<code>Mewtwo</code> 被釋放了,因為 <code>pokemon</code> 變量本身 - 它是唯一被強引用的 - 離開了作用域。
之後,當這個閉包被執行的時候,它列印出 <code>“Pikachu”</code>,因為,它是 <code>Pokemon</code> 在閉包被建立時候通過捕獲清單捕獲到的。
之後這個閉包被 <code>GCD</code> 釋放,是以這個 <code>Pikachu Pokemon</code> 被保留着。
相反,回到上面 <code>demo2</code> 的代碼:
之後,閉包隻是捕獲了對 <code>pokemon</code> 變量的引用,并不是真正的<code>Pikachu pokemon</code>變量包含的值。
是以,當 <code>pokemon</code> 之後被指派為一個新的值 <code>Mewtwo</code>,<code>Pikachu</code>,并且立即被釋放了。
但是這個 <code>pokemon</code> 變量 (在那時候,保留了<code>Mewtwo pokemon</code>)仍然被閉包強引用着。
是以,這就是 <code>pokemon</code> 被列印出的,當閉包在 1 秒之後被執行的時候。
并且那個 <code>Mewtwo</code> 僅僅被釋放一次,這個閉包之後被 <code>GCD</code> 釋放了。
是以...... 你全都掌握了麼?我知道,我們到此為止已經讨論了很多了......
這是一個更加人為的例子,同時混合了執行時定值和在閉包建立時捕獲的值 - 多謝捕獲清單 - 和捕獲變量的引用,和在閉包執行時定值:
你還能猜到這個的輸出結果麼?可能會比較難猜,但是這對你自己嘗試去确認輸出的内容來說是個非常好的練習,去檢查你是否掌握了今天所有的課程......
好吧,這裡就是代碼的輸出。你是不是正确了解了?
是以,這裡發生了什麼?變得更加複雜了,讓我們一步一步詳細道來:
<code>pokemon</code> 在初始化的時候被設值為 <code>Mew</code>
之後,1 号閉包被建立,并且 <code>pokemon</code> 的_值_被捕獲成一個新的 <code>capturedPokemon</code> 變量 - 它對于閉包來說是一個局部變量(并且 <code>pokemon</code> 變量的引用也被捕獲了,因為 <code>capturedPokemon</code> 和 <code>pokemon</code> 同時被閉包的代碼使用)
之後, <code>pokemon</code> 的值被修改為 <code>Mewtwo</code>
之後,2 号閉包被建立,并且 <code>pokemon</code>的_值_(那時候還是 <code>Mewtwo</code>)被捕獲成一個新的 <code>capturedPokemon</code> 變量 - 它對于閉包來說是一個局部變量(并且 <code>pokemon</code> 變量的引用也被捕獲了,因為他們同時被閉包的代碼使用)
現在,<code>demo8()</code> 方法函數結束了。
1 秒之後, GCD 開始執行第一個閉包(1 号閉包)。
列印出了這個_值_ <code>Mew</code>,它在第 2 步建立閉包的時候被 <code>capturePokemon</code> 捕獲
它也會對目前的 <code>pokemon</code> 變量定值,通過引用捕獲,它仍然是 <code>Mewtwo</code>(就和我們在第 5 步退出 <code>demo8()</code> 方法函數退出之前一樣)
之後,它把 <code>pokemon</code> 變量的值設定為 <code>Pikachu</code>(再一次,這個閉包捕獲了一個對變量 <code>pokemon</code> 的_引用_,是以這個和 <code>demo8()</code> 中使用的變量一樣,也和其他閉包一樣,它為這個變量指派。)
當這個閉包完成了執行,并且被 <code>GCD</code> 釋放,<code>Mew</code> 已不再被任何地方保留,是以他需要被釋放。但是 <code>Mewtwo</code> 仍然被第二個閉包的 <code>capturedPokemon</code> 捕獲着,并且 <code>Pikachu</code> 仍然儲存在 <code>pokemon</code> 變量中,它也被第二個閉包引用着。
另一個 1 秒之後,<code>GCD</code> 執行了第二個閉包(2 号閉包)。
列印出了這個_值_ <code>Mewtwo</code>,它在第 4 步建立閉包的時候被 <code>capturedPokemon</code> 捕獲。
它也對目前的 <code>pokemon</code> 變量定值,通過引用捕獲,是 <code>Pikachu</code>(因為它已經被 1 号閉包修改過了。)
最後,它把 <code>pokemon</code> 變量的值設定為 <code>Charizard</code>,并且這個 <code>Pikachu pokemon</code> 隻被那個 <code>pokemon</code> 變量引用,并且不在被任何人保留,是以它被釋放了。
當這個閉包完成了執行,并且被 <code>GCD</code> 釋放,這個 <code>capturedPokemon</code> 離開了本地的作用域,是以 <code>Mewtwo</code> 也被釋放了,并且 <code>pokemon</code> 變量已經不在被任何人引用,<code>Charizard pokemon</code> 也是,是以它也被釋放了。
仍然對所有的技巧感到困惑麼?那很正常。閉包的捕捉語義在某種成都上說是複雜的,特别是上面的那個精心策劃的例子。但是請記住下面這幾點:
<code>Swift</code> 閉包捕獲了一個對外部變量需要在閉包内部使用的一個_引用_。
那個引用在閉包被執行的時候獲得定值。
作為對這個變量的引用的捕捉(并且不是這個變量自身),你能從閉包内部修改這個變量的值(當然,如果這個變量被聲明為 <code>var</code> 并且不是 <code>let</code>)
相反,你能告訴 <code>Swfit</code> 在閉包建立的時候對這個變量定值 并且把這個_值_儲存在本地的一個靜态變量中,而不是捕獲變量本身。你可以通過使用捕獲清單,在括号内表達。
我會讓今天的課程結束,因為它可能很難了解。請不要猶豫去嘗試使用和測試這個代碼,或者在代碼編輯器裡修改他們,讓自己了清晰的了解所有的東西是怎麼運作的。
一旦你更加清晰的了解這些内容,那就是時候開始這個部落格的下一部分了,我們将讨論有關_弱_捕獲變量,為了防止循環引用,和在閉包中,到底什麼是 <code>[weak self]</code>,什麼是 <code>[unowned self]</code>。
對于知道 <code>Objective-C</code> 的讀者來說,你們能注意到,<code>Swift</code> 表現得和 <code>Objective-C</code> 的預設 <code>block</code> 文法不同,但是相反,它和在 <code>Objective-C</code> 中有 <code>__block</code> 修飾符的變量很像。↩
不像 <code>ObjC</code> 預設的表現...,更像是當你正在 <code>Objective-C</code> 中使用 <code>__block</code> ↩
請注意,即使在我們的例子中,我們僅捕獲了一個變量,你還是可以在捕獲清單中增加多個捕獲的變量,這就是為什麼它被叫做_清單_。當然,如果你沒有列出閉包參數清單,你講仍就能放置 <code>in</code> 這個關鍵字,在捕獲清單去從閉包體内分離他們之後。↩
<b>原文釋出時間為:2016年08月02日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>