これは?
Swiftのアノテーションである @ escaping と @ autoclosure がどんな役割を果たすのかについてまとめました。
@ escaping
Swiftのクロージャは参照型であるため、少し扱いには注意が必要です。
例えば、非同期処理を行う関数を宣言し、引数にクロージャの completionHandler (非同期処理の完了時に実行する処理)を取るとしましょう。
completionHandler は非同期処理が完了するまで待機しなければなりませんが、その前に関数のスコープを抜けると引数であるクロージャは解放されてしまいます。
それを防ぐために 関数のスコープから逃げる 、これが @ escaping です。
以下のように非同期処理の後にクロージャを実行する場合は、@ escaping を引数で受け取るクロージャに付与しなければなりません。
class AsyncClass {
func syncMethod(completionHandler: () -> Void) {
print("sync")
completionHandler()
}
func asyncMethod(completionHandler: @escaping () -> Void) {
print("async")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completionHandler()
}
}
}
class TestClass {
var tmp = 0
let asyncInstance = AsyncClass()
//selfを書かなくても良い
func syncTest() {
asyncInstance.syncMethod {
tmp = 100
}
}
//selfを明示的に書かなければならない
func asyncTest() {
asyncInstance.asyncMethod {
self.tmp = 100
}
}
deinit {
print("released")
}
}
@ escape でアノテートしているクロージャの場合、selfのキャプチャを明示的に示す必要があります。
この時に注意しなければならないのが、循環参照 というやつです。
試しに以下の処理を加えて見てください。
do {
let testInstance = TestClass()
testInstance.asyncTest()
}
doスコープを抜けるとき、本来なら testInstance
は解放され、 TestClass
の deinit
が呼び出されるはずですが、 ”released” という出力は確認できません。
これは、
-
AsyncClass
のasyncMethod
メソッドに引数として渡したクロージャがselfを強参照 - そのクロージャを
AsyncClass
のインスタンスが強参照 - selfもまた
AsyncClass
のインスタンスを強参照
しているため、 循環参照 が完成しているのです。
解決するために、クロージャの中のselfは 弱参照 にしてあげましょう。
func asyncTest() {
asyncInstance.asncMethod { [weak self] in
self?.tmp = 100
}
}
@ autoclosure
@ autoclosure は関数に渡された引数をクロージャにラップします。
なので、そのクロージャは原則引数は持ちません。
少しイメージがつきづらいと思うので、例を示します。
var customerInLine = ["Mike", "Jon", "Bob"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())")
}
serve(customer: { customerInLine.remove(at: 0)} )
順番待ちをするお客の配列 customerInLine
と、先頭のお客にサービスを提供する serve(customer:)
関数です。
ここで、serve関数の引数にわざわざクロージャを渡すのは煩わしいですね。
そんな時に役立つのが @ autoclosure です。
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())")
}
serve(customer: customerInLine.remove(at: 0))
このように、 customerInLine.remove(at: 0)
から、戻り値の String
を受け取るのではなく、クロージャとしてその処理を引数に取ることができるのです。
この @ autoclosure は以下のようなことを可能とします。
var customerInLine = ["Mike", "Jon", "Bob"]
var isBusy = true
func serve(customer customerProvider: @autoclosure () -> String) {
if isBusy {
print("Now busy, so please wait")
} else {
print("Now serving \(customerProvider())")
}
}
serve(customer: customerInLine.remove(at: 0))
print(customerInLine.count)
現在が忙しいならばお客に待ってもらうように処理を加えました。
これにより、 customerInLine.remove(at: 0)
の処理を実行せずに済みます。
serve
を呼び出した後もお客の数が 3
のままであることがその証拠です。
@ autoclosure は、引数として受け取る処理を遅延あるいは飛ばす可能性がある場合に用いると便利そうですね。
ただ、可読性の点から見ると使いすぎるのも良くなさそうです。