讓 closure 在 function 外繼續使用的 @escaping & self 副作用

@escaping 是個讓 closure 在 function 外繼續使用的特別語法。它有點難懂,但你卻不能忽略,因為 iOS SDK 裡不少 function 的參數都加了 @escaping。在了解它的神奇功能前,先讓我們看看以下例子,認識它幫我們解決的問題。

認識 @escaping

如圖所示,將 activity 儲存到 favoriteActivity 會產生紅色錯誤,這是因為 Swift 認為傳入 function 的 function 型別參數,比方例子裡印出打桌球的 closure,只是為了在 function 裡執行,function 執行完後就沒有利用價值,應該甩了它,不該在 function 執行完後繼續使用,所以它不讓我們將 activity 儲存到 favoriteActivity。若能儲存,即代表剛剛例子裡的 cuteBaby 在執行完 outsideActivity 後可以呼叫 favoriteActivity,執行當初 closure 的程式,印出打桌球。

想讓 Swift 知道我們對傳入 function 的 closure 動了真感情,想在 function 執行完後繼續使用,很簡單,只要在參數宣告前加上 @escaping 即可。英文 escape 的意思是逃脫,所以就像它名字表達的,@escaping 可讓 closure 從 function 逃脫,function 執行完後仍可繼續使用。

class Baby {
var name = "peter"
var favoriteActivity: (() -> Void)!
func outsideActivity(activity: @escaping () -> Void) {
activity()
favoriteActivity = activity
}
}

var cuteBaby = Baby()
cuteBaby.outsideActivity {
print("打桌球")
}
cuteBaby.favoriteActivity()
cuteBaby.favoriteActivity()
cuteBaby.favoriteActivity()

function outsideActivity 的參數 activity 加了 escaping,因此當 outsideActivity 執行完後,activity 的程式仍然可以使用。我們將 activity 存到 favoriteActivity,因此當 function outsideActivity 執行完後,我們可以繼續呼叫 favoriteActivity 打很多次桌球,總共會印出 4 次打桌球。

打桌球
打桌球
打桌球
打桌球

@escaping 的副作用: 存取自己的屬性或方法時須加 self

@escaping 幫我們解決問題,但也帶來一些副作用,就像止痛藥一樣。例如以下例子,當你想在 closure 的 { } 裡存取物件自己的屬性或方法時,必須額外加上 self,否則會產生紅色錯誤,出現以下錯誤訊息

Reference to property xxx in closure requires explicit use of self to make capture semantics explicit

加 self 跟 ARC 有關,因為加了 @escaping,closure 的程式之後還會執行,所以要加 self 提醒 ARC 增加 self 的 reference count,如此才不會在 closure 程式執行時 self 已經死掉,無法存取 self 的 property 或 method。

class Baby {
var name = "peter"

func outsideActivity(activity: @escaping () -> Void) {
activity()
}
}

class Mother {
var name = "wendy"
var child = Baby()

func play() {
child.outsideActivity {
print("\(self.name)和小孩\(self.child.name)打桌球")
}
}
}

iOS SDK 的 escaping 參數例子

URLSession 抓取網路資料的 function dataTask

func dataTask(with url: URL, 
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

參數 completionHandler 有 escaping, 為什麼呢 ?

let urlString = "https://wakelandtheatre.files.wordpress.com/2013/11/final-poster.jpg"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { data, response, error in
print("get data")

}.resume()
print("after dataTask")
}

以上程式會先印出 after dataTask,再印出 get data,因為 function dataTask 執行完會先回傳 task,然後我們呼叫 task 的 resume 啟動它,等資料抓到時才會執行 completionHandler。

因此參數 completionHandler 將在 function dataTask 執行完後過一段時間才執行,所以它要加上 escaping,如此才能在 function dataTask 執行完後執行 completionHandler。

也因為加了 escaping,我們在 closure 裡存取自己的 property imageView 時要加上 self,例如以下例子。

class DemoViewController: UIViewController {

@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://wakelandtheatre.files.wordpress.com/2013/11/final-poster.jpg"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data,
let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}.resume()
}
}
}

Timer 的動畫 function scheduledTimer

class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer

參數 block 有 escaping,為什麼呢 ?

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
print(timer.fireDate)
}
print("after scheduledTimer")

以上程式的 scheduledTimer 會先執行完,然後先印出 after scheduledTimer,之後再每隔 1 秒執行 closure 的程式,印出 timer 觸發的時間。

因此參數 block 將在 function scheduledTimer 執行完每隔 1 秒執行,所以它要加上 escaping,如此才能在 function scheduledTimer 執行完後執行 block。

也因為加了 escaping,我們在 closure 裡存取自己的 property label 時要加上 self,例如以下例子。

class DemoViewController: UIViewController {

@IBOutlet weak var label: UILabel!

override func viewDidLoad() {
super.viewDidLoad()

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.label.text = "\(timer.fireDate)"
}
}
}

沒有 escaping,存取自己的屬性或方法不須加 self

相反的,當參數沒有 @escaping 時,傳入的 closure 在存取自己的屬性時,你愛加不加 self 都可以。

class Baby {
var name = "peter"

func outsideActivity(activity: () -> Void) {
activity()
}
}

class Mother {
var name = "wendy"
var child = Baby()

func play() {
child.outsideActivity {
print("\(name)和小孩\(child.name)打桌球")
}
}
}

optional 的 function 型別參數預設是 @escaping

以上應該就是 @escaping 的故事吧。等等,當參數是 optional 時,故事又有了轉折。當參數是 optional 時,Swift 將認定此參數是 @escaping,所以在 closure 的 { } 裡存取物件自己的屬性時,一樣要加上 self。

class Baby {
var name = "peter"

func outsideActivity(activity: (() -> Void)?) {
if let activity {
activity()
}
}
}

class Mother {
var name = "wendy"
var child = Baby()

func play() {
child.outsideActivity {
print("\(self.name)和小孩\(self.child.name)打桌球")
}
}
}

iOS SDK 例子:

function present 的參數 completion 是 optional,因此預設是 escaping。

func present(_ viewControllerToPresent: UIViewController, 
animated flag: Bool, completion: (() -> Void)? = nil)

以下程式的 number 要加上 self,因為 completion 是 escaping。

class ViewController: UIViewController {
var number = 1

@IBAction func tap(_ sender: Any) {

let controller = UIViewController()
present(controller, animated: true) {
self.number += 1
}
}

}

值得注意的,當 function 型別的參數是 optional 時,它預設就是 escaping。如果我們畫蛇添足,另外再加上 @escaping, 將會出現可怕的紅色錯誤,顯示 Closure is already escaping in optional type argument

Allow implicit self for weak self captures, after self is unwrapped

在 @escaping closures 加上 implicit self 的 Swift 5.3

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com