解說
/*
⭐️ 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」這樣一個動作,doItLater
跟 doItNow
的寫法就不同:
doItLater 寫的是: { self.x = 100 } ,如果不指定 self 會出現 compiler 錯誤。
為什麼有這樣的區別呢 ❓
這牽涉到物件參照(object reference)的問題。假設: let a = A()
,這時執行: a.changeX()
這個物件方法就開始逐行執行:
doItLater { self.x = 100 }
當執行到 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),那就是程式設計師要考慮如何解決的另一個問題了。