🔰escaping closure

解說

/*
    ⭐️ type alias 
    =============

    為了方便起見,後面統一將「不須傳入參數、也沒有任何回傳」的函數稱為 `Task`,
    這樣可以讓程式看起來更清楚、簡潔。
*/
typealias Task = () -> Void

// ⭐️ 將「未來」才要執行的工作(函數)放在這裡
var tasks: [Task] = []

/*
    ⭐️ non-escaping closure
    =======================

    這是一個單純的函數,不管傳入什麼工作(task),都直接執行它(task()),
    所以當 doItNow 執行完畢,task 的生命週期也會隨著 doItNow 的結束而結束。

    像這樣的函數(task),我們就稱為 "non-escaping" closure。
    一般的 closure 參數,如果沒有特別標示,都是屬於這種。
*/
func doItNow(task: Task) {
    task()
}

/*
    ⭐️ escaping closure
    ===================

    這裡傳入的工作(task),是一個函數,它並沒有在 doItLater 這個函數
    內部執行(註:執行一個函數必須加 "()",例如:"task()"),而只是
    把 task 存到「未來要執行的工作」(tasks) 中。

    因為 doItLater 函數只做一件事,就是把 task 存到 tasks 中,
    所以直到 doItLater 執行完畢,task 這個函數都沒有真的執行過,
    不只如此,doItLater 執行完最後一行後,生命週期就會結束,但是
    task 這個函數不僅沒有結束,而且還繼續存活在 tasks 這個陣列中。

    這時會產生一個現象:

        「傳入的參數(task)存活得比處理它的函數(doItLater)還長」

    這時我們稱這個參數為 "escaping closure",在 Swift 的語法上
    必須標示 "@escaping" 這個關鍵字,不然會產生 compiler 錯誤。
*/
func doItLater(task: @escaping Task) {
    tasks.append(task)          // 執行完此行,doItLater 的生命週期就會結束,
}                               // 但 task 還會存活下來❗️


// 用來幫助說明 @escaping closure 這個概念的物件類別
class A {
    
    // 物件屬性
    var x = 10

    // 物件方法
    func changeX() {

        /*
            將 { self.x = 100 } 這件事,放到 tasks 陣列中,
            以後再做 (⭐️ 所以 x = 100 還不會真的發生❗️)。
        */
        doItLater { self.x = 100 }

        // 現在馬上做 { x = 200 } 這件事。
        doItNow { x = 200 }

    }
}

// 測試
let a = A()
a.changeX()
print(a.x)                  // 200


tasks.first?()              // ⭐️ 現在執行儲存起來的第一個工作({ self.x = 100 })
print(a.x)                  // 100

⭐️ 抓住物件

注意看:

同樣是「修改屬性 x」這樣一個動作,doItLaterdoItNow 的寫法就不同:

  • doItNow 寫的是: { x = 200 }

  • doItLater 寫的是: { self.x = 100 } ,如果不指定 self 會出現 compiler 錯誤。

為什麼有這樣的區別呢

這牽涉到物件參照(object reference)的問題。假設: let a = A(),這時執行: a.changeX()

這個物件方法就開始逐行執行:

  • doItLater { self.x = 100 }

  • doItNow { x = 200 }

當執行到 doItNow { x = 200 } 時,因為物件 a 顯然還活著, 所以如果要改變 a.x = 200 當然不是問題。

doItLater { self.x = 100 } 這一行,單純只是把 { self.x = 100 } 這項工作存起來而已,所以我們如何保證當「以後」真正執行時,self (也就是本例的 a) 物件還存活著

所以這裡就牽涉到一個 Swift 的絕活,它就是「硬性規定」你一定 要寫成 { self.x = 100 } 的形式,而不是 { x = 100 }, 來「抓住」(capture) self (也就是本例的 a) 物件,保證讓這個物件不會自動從記憶體中清除,所以將來執行 { self.x = 100 } 時才不會出問題。

物件抓得太緊

至於說「將物件抓住」這個做法會不會產生 strong reference cycle,導致物件無法清除,進而產生記憶體流失 (memory leak),那就是程式設計師要考慮如何解決的另一個問題了。

Last updated