引数のクロージャの属性【Swift実践入門読書メモ】

Swift実践入門を読んでみて、気になったところのメモです。

引数のクロージャの属性

  • 第4章 3節 pp.120-124

@escaping 属性

関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性です。

こんな感じで使われます。

func execClosure(_ closure: @escaping () -> Void) {
    ...
}

ということは、逆に言えば @escaping 属性がついていなければスコープ外で保持されることがないということです。そのため、 クロージャ内で self を参照していたりしても循環参照にならないため、 [weak self] 等を用いて弱参照にする必要がなくなります。

以前、以下のエントリで循環参照について考えた際には、どんな時に循環参照になってしまうのか考えるのが大変なので、問答無用で [weak self] を使用するのが楽そうと思いましたが、 @escaping 属性の有無で判別することができるようです。

komaji504.hateblo.jp

でも結局 @escaping 属性をつけるかどうかは人間が考える必要があって大変、ということはなく @escaping 属性をつけるべきときにつけていない場合は、コンパイルエラーとなるので気づくことができます。

class SampleClass {
    var closure: (() -> Void)?
    func execClosure(_ closure: () -> Void) {
        self.closure = closure
    }
}
Playground execution failed: error: SampleApp.playground:4:24: error: assigning non-escaping parameter 'closure' to an @escaping closure
        self.closure = closure
                       ^
SampleApp.playground:3:24: note: parameter 'closure' is implicitly non-escaping
    func execClosure(_ closure: () -> Void) {
                       ^
                       @escaping

そのため、@escaping 属性がついていれば循環参照の恐れがあるので、クロージャによってキャプチャされる変数は [weak self] 等を用いて弱参照にしておく、ついていなければ強参照のまま利用するというようにすると良さそうです。

@autoclosure 属性

引数をクロージャで包むことで遅延評価を実現するための属性です。

引数の遅延評価が有効なケース

以下の printResults(result1:result2:) は、第一引数の値が true なら最初の分岐以降の処理は実行されません。

func printResults(result1: Bool, result2: Bool) {
    if result1 {
        print("result1 is true")
    } else if result2 {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }

処理の順序はこんな感じになります。

  • result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

ですが、以下のように引数に関数の実行結果を直接渡している場合には、渡された関数は関数内の処理に入る前に実行されます。

printResult(result1: result1(), result2: result2())
  • result1() を評価する ← 関数の処理前
  • result2() を評価する ← 関数の処理前
  • result1() の結果である result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2() の結果である result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

このとき、 result1() の結果が true になるのであれば result2() の評価は無駄になってしまいます。
これを防ぐために遅延評価が有効となります。

クロージャで包む

関数の実行結果を直接渡すと関数内の処理に入る前に実行されてしまいますが、以下のようにクロージャで包むと遅延評価を行うことができます。

func printResults(result1: Bool, result2: () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: { return result2() })

ですが、これだと printResults(result1:result2:) のインターフェースが変わってしまいます(第二引数に渡す型が Bool から () -> Bool に変わっている) 。
そこで @autoclosure の登場です。

@autoclosure をつけると暗黙的にクロージャに包んでくれるので、関数のインターフェースは変わらずに関数を実行することができます。

func printResults(result1: Bool, result2: @autoclosure () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: result2())

@autoclosure をつけるとインターフェースが変わらないのは確かに良いなあと思いました。

ですが、あまり遅延評価を要するコードを書く機会がない (書くべきところに気づけていない?) ので、自分が使うことはあまりないような気がしました。

合わせて読みたい

  • 弱参照による循環参照への対処
    • 第11章 2節 pp.274-275
  • クロージャ
    • 第11章 3節 pp.276-285