Swift
循環参照

Swiftでクロージャを書く時に循環参照を避けつつ安全に非Optionalにしてみる

ギョムでコードを書いていて、質問が来て、どうやら認知度は高くないというテクだったらしいので折角なのでシェアしてみる。

Swiftでのクロージャの構文はこう。

class Foo {
  func bar() {
    let x: (Int) -> Int = { a in a + 1 }
  }
}

引数をinの前に列挙する。宣言時に参照可能な変数をキャプチャすることも出来る。この時対象のretain countが増える。

class Foo {
  func bar() {
    let b = 2
    let x: (Int) -> Int = { a in a + b }
  }
}

retain countが増えるので、selfを参照し、クロージャをキャプチャしてしまうと循環参照が生まれることがある。

class Foo {
  var inc: Int = 2
  var myClosure: ((Int) -> Int)?

  func bar() {
    myClosure = { a in a + self.inc } // self <-> myClosure の循環参照ができる。
  }
}

これを避けるのに一般に使われる方法が、unowned, weakだ。前者はクラッシュの危険があり、後者はOptionalになる。
これらを用いて最も安全に書くなら、 weakguardを組み合わせる方法だろうか。

class Foo {
  var inc: Int = 2
  var myClosure: ((Int) -> Int)?

  func bar() {
    myClosure = { [weak self] a in 
     guard let `self` = self else { fatalError() } // 発生しないと保証する。こうするならunownedでも良い。
     return a + self.inc 
    }
  }
}

実はletプロパティなら、変更されないことが保証されているため、weakunownedも使わない方法がある。

class Foo {
  let inc: Int = 2
  var myClosure: ((Int) -> Int)?

  func bar() {
    myClosure = { [inc] a in a + inc }
  }
}

キャプチャされるのはincなので、selfのリテインカウントは増えない。
incは値型なのでこの場合はクロージャの宣言時にコピーされる。

注意が必要なのが、プロパティをvarで宣言している場合、クロージャの宣言時に存在したプロパティを対象にしているので変更しても反映されなくなってしまうという点。これはプロパティの種類が値型かクラスであるかは関係なく、どちらでも発生する。letに限定して使うのが良いだろう。

クロージャの中に「init以降変更することのないプロパティ」を渡したい、という場面は多くあるはず。
毎回guardして長いコードを書いたり、unownedを使ってクラッシュに怯えるならば、この方法使ってみては如何でしょうか。