可選型的非逃逸閉包

2021-09-17 21:52:43 字數 3268 閱讀 4028

簡單來說,是為了管理記憶體。乙個閉包會強引用它捕獲的所有物件————如果你在閉包中訪問了當前物件中的任意屬性或例項方法,閉包會持有當前物件,因為這些方法和屬性都隱性地攜帶了乙個self引數。

然而,使用非逃逸的閉包不會產生迴圈引用————編譯器可以保證在函式返回時閉包會釋放它捕獲的所有物件。因此,編譯器只要求在逃逸閉包中明確對self的強引用。顯然,使用非逃逸閉包是乙個更加愉悅的方案。

使用非逃逸閉包的另乙個好處是編譯器可以應用更多強有力的效能優化。例如,當明確了乙個閉包的生命週期的話,就可以省去一些保留(retain)和釋放(release)的呼叫。此外,如果閉包是乙個非逃逸閉包,它的上下文的記憶體可以儲存在棧上而不是堆上————雖然我不確定當前的編譯器是否執行了這個優化(一篇公布於 2016 年 3 月的錯誤報告顯示當時並沒有執行)。

class dispatchqueue
在 swift 3 之前,完全是另外一回事:逃逸是預設狀態,你可以新增@noescape來覆蓋此狀態。新的行為更好,因為在預設狀態下是安全的:遇到有潛在迴圈引用的情況時,乙個方法呼叫必須顯式地予以標註。因此,@escaping識別符號還有警示開發者的作用。

關於非逃逸的閉包有乙個預設規則:它只能應用到即時函式的引數列表位,也就是說任何作為引數傳入的閉包。所有其他型別的閉包都是逃逸的。

讓我們看一些示例。最簡單的情況就像 map:這個函式接受乙個立即執行的閉包引數。正如我們所看到的,這個閉包是乙個非逃逸的(我從 map 的真實簽名中省略了一些無關、不重要的細節):

func map(_ transform: (iterator.element) -> t) -> [t]
函式型別的變數總是逃逸的

與此相比。即使沒有明確的標註,指向/儲存函式型別(閉包)的變數或屬性,都是自動逃逸的(實際上,如果你顯式新增乙個 @escaping 也會報錯)。這其實很合理,因為賦值給乙個變數隱性地允許該值逃逸到變數的作用域中,而非逃逸閉包不允許這種行為。這可能會讓人困惑,但乙個未做任何標註的閉包在引數列表中與其他任何情況都不同。

可選型的閉包總是逃逸的

更令人驚訝的是,即便閉包被用作引數,但是當閉包被包裹在其他型別(例如元組、列舉的 case 以及可選型)中的時候,閉包仍舊是逃逸的。由於在這種情況下閉包不再是即時的引數,它會自動變成逃逸閉包。因此,在 swift 3.0 中,當你編寫乙個接受函式型別引數的函式時,該引數不能同時是可選型和非逃逸的。思考下面這個精心設計的例子:函式transform接受乙個整數 n 以及乙個可選型的變換函式 f。正常情況下它返回 f(n),而 f 為空值時返回 n。

/// returns `n` unchanged if `f` is nil.

func transform(_ n: int, with f: ((int) -> int)?) -> int

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 = ) -> int

使用過載提供乙個可選型和乙個非逃逸的變體

如果不能提供預設值,michael ilseman 建議使用過載解決————你可以編寫兩個版本的方法,乙個帶有可選型(逃逸)函式引數,另乙個帶有非可選型的非逃逸引數:

// overload 1: optional, escaping

func transform(_ n: int, with f: ((int) -> int)?) -> int

return f(n)

}// overload 2: non-optional, non-escaping

func transform(_ input: int, with f: (int) -> int) -> int

我新增了一些列印語句來演示哪個函式被呼叫。用不同的引數來測試一下。不出意外,當你傳入nil,型別檢查器選擇第乙個過載的版本,因為只有它相容輸入的引數型別:

swfit

transform(10, with: nil) // → 10

// using optional overload

如果你傳遞乙個可選函式型別的閉包,同樣如此:

let f: ((int) -> int)? = 

transform(10, with: f) // → 20

// using optional overload

即便變數的值不是可選型的,swift 依舊選擇第乙個版本的過載。這是因為儲存在變數中的函式是自動逃逸的,因此與期望傳入非逃逸引數的第二個過載版本不相容:

let g: (int) -> int = 

transform(10, with: g) // → 20

// using optional overload

但是,當你傳遞乙個閉包的表示式,即函式字面量到相應的位置時,情況會變得不一樣。此時會選擇第二個非逃逸的版本:

transform(10)  // → 20

// using non-optional overload

現在使用字面量的閉包表示式來呼叫高階函式的方式已經習以為常,所以在大多數情況下你都可以選用這個令人愉悅的方式(即非逃逸,不需要擔心迴圈引用),同時仍然可以選擇傳入nil。如果你決定這麼做,一定要在文件中明確標註你需要兩個過載的理由。

型別別名總是逃逸的

最後要注意的是,在 swift 3.0 中,你不能向typealiases中新增逃逸或者非逃逸的標註。如果你在函式宣告中對乙個函式型別的引數使用了型別別名(typealias),這個引數總會被視為逃逸的。這個 bug 已經在主分支上修復了,應該會出現在下乙個 release 版本中。

Swift3 非 逃逸閉包

swift3.0更新以後對閉包的屬性進行了調整,之前預設的是逃避策略,如果是乙個非逃逸閉包則需加上 noescape。在更新之後,則恰恰相反。這兩種屬性的區別在於,non escape function執行完,closure也執行結束,closure裡面的物件不會被retain 反之,closure...

iOS Swift 逃逸閉包

逃逸閉包必須滿足下面2個條件 1 閉包作為乙個引數傳到函式中 2 閉包在函式返回之後才執行 需要在引數前面加入標註 escaping,用來指明這個閉包是允許 逃逸 出這個函式的。注意 將乙個閉包標記為 escaping 意味著你必須在閉包中顯式地引用 import uikit 逃逸閉包滿足下面2個條...

Swift 逃逸閉包

一般在定義網路請求框架時,會宣告成功閉包,失敗閉包。用來 返回的資料給呼叫者。成功閉包,失敗閉包當然是作為請求方法的引數,這時候會報錯,因為這種閉包必須宣告為逃逸閉包。也就是在閉包型別前加 escaping 當乙個閉包作為引數傳到乙個函式中,但是這個閉包在函式返回之後才被執行,我們稱該閉包從函式中逃...