Swift structのクロージャーが変数をキャプチャする
初めてクロージャーを見たときこそ関数とどう違うのかわからなかったが、 普段の開発では特に気にせず普通に使っていた。
RxSwiftのObserverクラスを見てみると少し不安になってきた。
struct Observer { var eventHandler: () -> () init(_ eventHandler: @escaping () -> ()) { self.eventHandler = eventHandler } }
RxSwiftのAnyObserverやBinder、ControllTargetといったObserverTypeはみな上のようなstructで定義されていて、内部にクロージャーを保持している。
そこでちょっと気になる点があって実際に手を動かしてみたところ私の理解が全然できていないことが判明したのでメモしておく。
#
クロージャーが変数をキャプチャすると言うのは知っていて、普段それを意識するタイミングがあるとすれば、循環参照を避けるためにweakを使った弱参照をする時である。
final class Controller { private let session = APISession() func fetch() { session.fetch { [weak self] result in self?.hogehoge } } }
その点structを使えば循環参照など気にならずコピーが発生する必要もない。 上に書いたObserverと同様のCounterを定義してみる。
struct Counter { var countUp: () -> () init(_ countUp: @escaping () -> ()) { self.countUp = countUp } }
var count = 0 counter = Counter { count += 1 print("count: \(count)") }
counter.countUp() // count: 1 counter.countUp() // count: 2 counter.countUp() // count: 3
var count = 0 counter = Counter { count += 1 print("count: \(count)") } count = 100
observer.eventHandler() // count: 101 observer.eventHandler() // count: 102 observer.eventHandler() // count: 103
では次の場合はどうだでだろうか。
var count = 0 let counterA = Counter { count += 1 print("count: \(count)") } let counterB = counterA
counterA.countUp() // count: 1 counterB.countUp() // count: 2 counterB.countUp() // count: 3 counterA.countUp() // count: 4
な、なんと。 Couterが内部でcountを保持しているとは言え、どちらもstructであるので、コピーが発生する。 よってcounteAとcounterBはそれぞれ独立してcountUpすると思っていた。
やっぱり理解不足だ。 調べたらドキュメントに同じ例で書いてあって
https://docs.swift.org/swift-book/LanguageGuide/Closures.html#
Capturing Valuesの部分。
何よりクロージャーがreference typeであることを初めて知った。
ちなみじドキュメントを読むとクロージャーが変数をキャプチャする最もシンプルな例はnested functionで
func increment(by amount Int) -> () -> Int { var count = 0 func countUp() { count += amount } return countUp() }
let incrementByTen = increment(by: 10) let alsoIncrementByTen = incrementByTen incrementByTen() // 10 alsoIncrementByTen() // 20
とクロージャーが参照型であることが書かれていた。