iOS
Swift
annotation

【Swift】@escapingや@autoclosureについて

これは?

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 は解放され、 TestClassdeinit が呼び出されるはずですが、 ”released” という出力は確認できません。

これは、

  1. AsyncClassasyncMethod メソッドに引数として渡したクロージャがselfを強参照
  2. そのクロージャを AsyncClass のインスタンスが強参照
  3. 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 は、引数として受け取る処理を遅延あるいは飛ばす可能性がある場合に用いると便利そうですね。
ただ、可読性の点から見ると使いすぎるのも良くなさそうです。

参考

Apple Inc. “The Swift Programming Language" -Closures