做者:Ole Begemann,原文連接,原文日期:2016/10/10
譯者:Cwift;校對:walkingway;定稿:CMBhtml
Swift 的閉包分爲 逃逸 與 非逃逸 兩種。一個接受逃逸閉包做爲參數的函數,逃逸閉包(可能)會在函數返回以後才被調用————也就是說,閉包逃離了函數的做用域。git
逃逸閉包一般與異步控制流相關聯,以下例所示:github
一個函數開啓了一個後臺任務後當即返回,而後經過一個完成回調(completion handler)報告後臺任務的結果。算法
一個視圖類把『按鈕點擊事件執行的操做』封裝成一個閉包,並存儲爲自身的屬性。每次用戶點擊按鈕時,都會調用該閉包。閉包會逃離屬性的設置器(setter)。swift
你使用 [DispatchQueue.async]() 在派發隊列(dispatch queue)上安排了一個異步執行的任務。這個閉包任務的生命週期會比 async
的做用域活得更長久。api
與之對應的 DispatchQueue.sync,它會一直等到任務閉包執行完畢後才返回——閉包永遠不會逃逸。map 以及標準庫中其餘的序列和數組的算法也是非逃逸的。數組
簡單來講,是爲了管理內存。一個閉包會強引用它捕獲的全部對象————若是你在閉包中訪問了當前對象中的任意屬性或實例方法,閉包會持有當前對象,由於這些方法和屬性都隱性地攜帶了一個 self
參數。安全
這種方式很容易致使循環引用,這解釋了爲何編譯器會要求你在閉包中顯式地寫出對 self
的引用。這迫使你考慮潛在的循環引用,並使用捕獲列表手動處理。性能優化
然而,使用非逃逸的閉包不會產生循環引用————編譯器能夠保證在函數返回時閉包會釋放它捕獲的全部對象。所以,編譯器只要求在逃逸閉包中明確對 self
的強引用。顯然,使用非逃逸閉包是一個更加愉悅的方案。閉包
使用非逃逸閉包的另外一個好處是編譯器能夠應用更多強有力的性能優化。例如,當明確了一個閉包的生命週期的話,就能夠省去一些保留(retain)和釋放(release)的調用。此外,若是閉包是一個非逃逸閉包,它的上下文的內存能夠保存在棧上而不是堆上————雖然我不肯定當前的編譯器是否執行了這個優化(一篇公佈於 2016 年 3 月的錯誤報告顯示當時並無執行)。
從 Swift 3.0 開始,非逃逸閉包變成了閉包參數的默認形式。若是你想容許一個閉包參數逃逸,須要給這個類型增長一個 @escaping
的標註。例如, DispatchQueue.async
(逃逸)和 DispatchQueue.sync
(非逃逸)的定義:
class DispatchQueue { ... func async(/* other params omitted */, execute work: @escaping () -> Void) func sync<T>(execute work: () throws -> T) rethrows -> T }
在 Swift 3 以前,徹底是另一回事:逃逸是默認狀態,你能夠添加 @noescape
來覆蓋此狀態。新的行爲更好,由於在默認狀態下是安全的:遇到有潛在循環引用的狀況時,一個方法調用必須顯式地予以標註。所以,@escaping
標識符還有警示開發者的做用。
關於非逃逸的閉包有一個默認規則:它只能應用到即時函數的參數列表位,也就是說任何做爲參數傳入的閉包。全部其餘類型的閉包都是逃逸的。
讓咱們看一些示例。最簡單的狀況就像 map:這個函數接受一個當即執行的閉包參數。正如咱們所看到的,這個閉包是一個非逃逸的(我從 map 的真實簽名中省略了一些無關、不重要的細節):
func map<T>(_ transform: (Iterator.Element) -> T) -> [T]
與此相比。即便沒有明確的標註,指向/保存函數類型(閉包)的變量或屬性,都是自動逃逸的(實際上,若是你顯式添加一個 @escaping 也會報錯)。這其實很合理,由於賦值給一個變量隱性地容許該值逃逸到變量的做用域中,而非逃逸閉包不容許這種行爲。這可能會讓人困惑,但一個未作任何標註的閉包在參數列表中與其餘任何狀況都不一樣。
更使人驚訝的是,即使閉包被用做參數,可是當閉包被包裹在其餘類型(例如元組、枚舉的 case 以及可選型)中的時候,閉包仍舊是逃逸的。因爲在這種狀況下閉包再也不是即時的參數,它會自動變成逃逸閉包。所以,在 Swift 3.0 中,當你編寫一個接受函數類型參數的函數時,該參數不能同時是可選型和非逃逸的。思考下面這個精心設計的例子:函數 transform
接受一個整數 n 以及一個可選型的變換函數 f。正常狀況下它返回 f(n),而 f 爲空值時返回 n。
/// Applies `f` to `n` and returns the result. /// Returns `n` unchanged if `f` is nil. func transform(_ n: Int, with f: ((Int) -> Int)?) -> Int { guard let f = f else { return n } return f(n) }
這裏函數 f 是逃逸的,由於 ((Int) -> Int)?
是 Optional<(Int) -> Int>
的縮寫,即函數類型不在一個即時參數位上。
Swift 團隊已經意識到了這個問題,而且會在未來的版本中解決它。在那以前,對這個問題有必定了解是很是重要的。目前沒有辦法讓一個可選型的閉包變成非逃逸的,可是在許多狀況下,你能夠經過爲閉包提供一個默認值的方式來避免使用可選型參數。在咱們的例子中,默認值是一個特定的函數,返回一個不可變的參數:
/// Uses a default implementation for `f` if omitted func transform(_ n: Int, with f: (Int) -> Int = { $0 }) -> Int { return f(n) }
若是不能提供默認值,Michael Ilseman 建議使用重載解決————你能夠編寫兩個版本的方法,一個帶有可選型(逃逸)函數參數,另外一個帶有非可選型的非逃逸參數:
// Overload 1: optional, escaping func transform(_ n: Int, with f: ((Int) -> Int)?) -> Int { print("Using optional overload") guard let f = f else { return n } return f(n) } // Overload 2: non-optional, non-escaping func transform(_ input: Int, with f: (Int) -> Int) -> Int { print("Using non-optional overload") return f(input) }
我添加了一些打印語句來演示哪一個函數被調用。用不一樣的參數來測試一下。不出意外,當你傳入 nil
,類型檢查器選擇第一個重載的版本,由於只有它兼容輸入的參數類型:
swfit transform(10, with: nil) // → 10 // Using optional overload
若是你傳遞一個可選函數類型的閉包,一樣如此:
let f: ((Int) -> Int)? = { $0 * 2 } transform(10, with: f) // → 20 // Using optional overload
即使變量的值不是可選型的,Swift 依舊選擇第一個版本的重載。這是由於存儲在變量中的函數是自動逃逸的,所以與指望傳入非逃逸參數的第二個重載版本不兼容:
let g: (Int) -> Int = { $0 * 2 } transform(10, with: g) // → 20 // Using optional overload
可是,當你傳遞一個閉包的表達式,即函數字面量到相應的位置時,狀況會變得不同。此時會選擇第二個非逃逸的版本:
transform(10) { $0 * 2 } // → 20 // Using non-optional overload
如今使用字面量的閉包表達式來調用高階函數的方式已經習覺得常,因此在大多數狀況下你均可以選用這個使人愉悅的方式(即非逃逸,不須要擔憂循環引用),同時仍然能夠選擇傳入 nil
。若是你決定這麼作,必定要在文檔中明確標註你須要兩個重載的理由。
最後要注意的是,在 Swift 3.0 中,你不能向 typealiases
中添加逃逸或者非逃逸的標註。若是你在函數聲明中對一個函數類型的參數使用了類型別名(typealias),這個參數總會被視爲逃逸的。這個 bug 已經在主分支上修復了,應該會出如今下一個 release 版本中。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。