Swift 3 の @escaping とは何か


以前 Swiftの @noescape をもっと使おう という記事を書いたのですが、 Swift 3 からは @noescape の挙動がデフォルトに、そして@noescape の記述はdeprecatedになり、代わりに @escaping が追加されました。

使い方

引数で受け取るクロージャに対してattributeとして付与します。

class A {
    private let storedClosure: () -> ()

    init(closure: @escaping () -> ()) { // ここ
        storedClosure = closure
    }
}

どういうときに必要か

クロージャがスコープから抜けても存在し続けるときに @escaping が必要になります。
具体的には以下のような場合です。

  • クロージャがスコープ外で強参照されるとき
  • クロージャを非同期的に実行するとき

よってクロージャをすぐに実行し、どこからも強参照されない場合は @escaping は必要ありません。

class A {
    init(closure: () -> ()) { // ここ
        closure()
    }
}

escapingするとどうなるか

self. が必要

@escaping なクロージャ内でselfの変数やメソッドを使用する場合、selfをキャプチャすることを明示するため self. を付ける必要があります。

循環参照に気をつける

@escaping なクロージャはどこかから強参照される可能性があります。その参照元をクロージャ内で強参照すると循環参照になるので気をつけましょう。

特に、selfがプロパティとしてクロージャを保持 (強参照) する場合、クロージャ内で self を強参照すると循環参照になるので注意が必要です。

クロージャ内でオブジェクトを弱参照したい場合は、そのオブジェクトに対して weakunowned を付けます。

良いコード例

以下の例では [weak self] を使用することで循環参照を防いでいます。よって最終的に b オブジェクトが破棄され "deinit" が出力されます。

class A {
    private let storedClosure: () -> ()

    init(closure: @escaping () -> ()) {
        storedClosure = closure
    }
}

class B {
    private var a: A?
    private var count = 0

    func doSomething() {
        a = A(closure: { [weak self] in // selfを弱参照する
            self?.count += 1
        })
    }

    deinit {
        print("deinit") // 呼ばれる
    }
}

do {
    let b = B()
    b.doSomething()
}

悪いコード例

以下の例では、このようにして循環参照が発生します。

  1. b オブジェクトが a オブジェクトを保持 (強参照)
  2. a オブジェクトがクロージャを保持 (強参照)
  3. クロージャが self. として b オブジェクトをキャプチャして強参照

よって b オブジェクトは破棄されず "deinit" が出力されません。

class A {
    private let storedClosure: () -> ()

    init(closure: @escaping () -> ()) {
        storedClosure = closure
    }
}

class B {
    private var a: A?
    private var count = 0

    func doSomething() {
        a = A(closure: {
            self.count += 1 // selfを強参照している
        })
    }

    deinit {
        print("deinit") // 呼ばれない
    }
}

do {
    let b = B()
    b.doSomething()
}

参考