Swift中轉義閉包示例詳解

前言

Swift 是一種非常強大的編程語言,是為 Apple 生態系統開發應用程序的首選;iOS、macOS、watchOS 和 tvOS。作為使用 Swift 編寫代碼的開發人員,我們經常使用閉包;語言的一個重要而重要的章節。

閉包不是初學者開始的主題。然而,這是每個人都必須盡快瞭解的東西。有很多方面需要瞭解並瞭解它們的工作原理。在所有這些中,有一個特定的;轉義閉包和@escaping屬性。在這篇文章中,我將盡可能簡單地解釋它們是什麼以及它們可能帶來的附帶影響。

轉義與非轉義閉包

在談論轉義閉包時,我們總是指作為函數或方法參數提供的閉包。一般來說,我們將提供給方法(或函數)的閉包分為兩類:

  1. 在方法執行完成之前調用的閉包。
  2. 在方法執行完成後調用的閉包。

在後一種情況下,我們談論的是轉義閉包;關閉該繼續即使電子住後的的xecution方法,直到我們在以後的任何時間在未來給他們打電話。

在前一種情況下,與我上面描述的完全相反,我們稱閉包為non-escaping。

直到 Swift 3,默認情況下,所有作為參數傳遞給方法或函數的閉包都被認為是轉義的。自 Swift 3 以來,這不再正確;默認情況下,所有方法都被認為是非轉義的,這意味著它們在方法執行完成之前被調用。

以下代碼部分演示瞭一個非轉義閉包:

func  add(num1:  Double,

 num2:  Double,

 completion:  (_  result:  Double)  ->  Void)  {

    let  sum  =  num1  +  num2

    completion(sum)

}

所述completion封閉件之前執行代碼葉調用的方法,所以這不是一個逸出閉合的情況。

然而,一個閉包是如何從一個方法中逃脫的,所以我們最終得到瞭與上述情況相反的結果?

逃離方法

為瞭使閉包成為轉義閉包,有必要將對其的引用保留在方法的范圍之外,以便我們稍後使用它。看看下面的代碼:

class  Demo  {  
    var  result:  Double?
    var  resultHandler:  (()  ->  Void)?
    func  add2(num1:  Double,
              num2:  Double,
              completion:  ()  ->  Void)  {
        resultHandler  =  completion
        result  =  num1  +  num2
    }
}

這裡我們有一個result屬性,它保存在方法內部發生的加法的結果。但我們也resultHandler有財產;this 保持對completion作為方法參數提供的閉包的引用。

閉包通過以下行從方法中轉義:

resultHandler  =  completion

然而,這不是唯一需要的操作,所以我們可以說這completion是一個轉義閉包。我們必須明確指出編譯器,否則我們將在 Xcode 中看到以下錯誤:

為瞭修復它,我們需要用@escaping屬性標記閉包。我們將此屬性放在閉包名稱和分號之後,但在閉包類型之前,如下所示:

func  add2(num1:  Double,
          num2:  Double,
          completion:  @escaping  ()  ->  Void)  {
    ...
}

編譯器不再抱怨,completion現在正式成為轉義閉包。

將轉義關閉付諸行動

讓我們在上面的Demo類中再添加兩個方法;一個將調用add2(num1:num2:completion:)方法,另一個將調用resultHandler閉包以獲得最終結果:

class  Demo  {
    ...
    func  doubleSum(num1:  Double,
                    num2:  Double)  {
        add2(num1:  num1,  num2:  num2)  {
            guard let  result  =  self.result  else  {  return  }
            self.result  =  result  *  2
        }
    }

    func  getResult()  {
        resultHandler?()
    }
}

第一種方法將 add 方法計算的結果加倍。但是,該結果不會翻倍,並且在add2(num1:num2:completion:)我們調用該getResult()方法之前,不會執行該方法中閉包主體內的代碼。

這是我們從轉義閉包中受益的地方;我們可以在我們的代碼中需要的時候以及在合適的時機觸發閉包的調用。盡管提供的示例故意過於簡單,但在實際項目中,實際優勢會變得更加明顯和大膽。

註意強參考周期

讓我們為Demo類添加最後一個,並實現默認的初始化器和析構器方法:

class  Demo  {
    init()  {
        print("Init")
    }
    deinit  {
        print("Deinit")
    }
    ...
}

init()是Demo初始化實例時調用的第一個方法,deinit也是釋放實例之前調用的最後一個方法。我向它們都添加瞭一個打印命令,以驗證它們是否被調用,並且在使用帶有上述轉義閉包的方法時沒有內存泄漏。

註意:當我們嘗試通過將對象設置為 nil 來釋放它時,可能存在內存泄漏,但該對象仍保留在內存中,因為該對象與其他保持其活動狀態的對象之間存在強引用。

現在,讓我們添加以下幾行來使用上述所有內容:

var  demo:  Demo?  =  Demo()
demo?.doubleSum(num1:  5,  num2:  10)
demo?.getResult()
print((demo?.result!)!)
demo  =  nil

首先,我們初始化類的一個可選實例Demo,以便稍後我們可以將其設為 nil。然後,我們調用該doubleSum(num1:num2:)方法以將作為參數給出的兩個數字相加,然後將該結果加倍。但是,正如我之前所說的,在我們調用該getResult()方法之前不會發生這種情況;在方法中實際調用轉義閉包的那個add2(num1:num2:completion:)。

最後,我們打印實例中result屬性的值demo,並將其demo設為 nil。

*註意:*如上面的代碼片段所示,使用感嘆號 (!) 強制展開可選值是一種非常糟糕的做法,請不要這樣做。我在這裡這樣做的唯一原因是為瞭讓事情盡可能簡單。

以上行將打印以下內容:

Init

30.0

請註意,此處缺少“Deinit”消息!也就是說deinit沒有調用該方法,證明制作demo實例nil沒有實際結果。看起來,隻需幾行簡單的代碼,我們就設法解決瞭內存泄漏問題。

內存泄漏背後的原因

在我們找到解決內存泄漏的方法之前,有必要瞭解它發生的原因。為瞭找到它,讓我們退後幾步來修改我們之前所做的。

首先,我們使用以下completion行使閉包從方法中逃逸:

resultHandler  =  completion

這條線比看起來更“有罪”,因為它創建瞭對閉包的強烈引用completion。

註意:閉包是引用類型,就像類一樣。

然而,僅憑這一點還不足以產生問題,因為釋放demo實例會刪除對閉包的引用。真正的麻煩始於doubleSum(num1:num2:)方法內部的閉包主體。

在那裡,我們這次通過在使用對象訪問屬性時捕獲**對象來創建另一個從閉包到demo實例的強引用:selfresult

guard let  result  =  self.result  else  {  return  }
self.result  =  result  *  2

當它們都到位時,Demo 實例保持對閉包的強引用,而閉包則是對實例的強引用。這會創建一個保留循環,也稱為強引用循環。發生這種情況時,每個引用類型都會使另一個引用類型在內存中保持活動狀態,因此它們最終都不會被釋放。

請註意,這僅發生在包含帶有轉義閉包的方法的類中。structs 的情況有所不同,因為它們不是引用而是值類型,並且顯式引用self不是強制性的。

消除強引用循環

有兩種方法可以避免強引用循環,從而避免內存泄漏。第一個是在我們調用閉包後手動且顯式地釋放對閉包的引用:

func  getResult()  {
    resultHandler?()
    // Setting nil to resultHandler removes the reference to closure.
    resultHandler  =  nil
}

第二種方法是在閉包的主體中弱**捕獲self實例:

func  doubleSum(num1:  Double,
 num2:  Double)  {
    add2(num1:  num1,  num2:  num2)  {  [weak  self]  in
        guard let  result  =  self?.result  else  {  return  }
        self?.result  =  result  *  2
    }
}

[weak self] in在關閉打開後查看添加。有瞭這個,我們建立瞭對 Demo 實例的弱引用,因此我們避免瞭保留循環。請註意,我們將self用作可選值,並在其後加上問號 (?) 符號。

沒有必要應用這兩種更改以避免強引用循環。無論我們最終選擇哪一個,從現在開始,輸出也將包含“Deinit”消息。這意味著該demo對象變為 nil,並且我們不再有內存泄漏。

Init

30.0

Deinit

概括

離開這裡需要帶上一件事,那就是在使用轉義閉包時要小心。無論您是實現自己的接受轉義閉包作為參數的方法,還是使用具有轉義閉包的 API,請始終確保不會以強引用循環結束。在開發應用程序時,內存泄漏是一個很大的“禁忌”,我們當然不希望我們的應用程序在某個時候崩潰或因此被系統終止。另一方面,不要猶豫使用轉義閉包;它們提供瞭可以產生更強大代碼的優勢。

到此這篇關於Swift中轉義閉包的文章就介紹到這瞭,更多相關Swift轉義閉包內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: