Swift structのクロージャーが変数をキャプチャする

初めてクロージャーを見たときこそ関数とどう違うのかわからなかったが、 普段の開発では特に気にせず普通に使っていた。

RxSwiftのObserverクラスを見てみると少し不安になってきた。

 

struct Observer {
    var eventHandler: () -> ()
    init(_ eventHandler: @escaping () -> ()) {
        self.eventHandler = eventHandler
    }
}

RxSwiftのAnyObserverやBinder、ControllTargetといったObserverTypeはみな上のようなstructで定義されていて、内部にクロージャーを保持している。

github.com

そこでちょっと気になる点があって実際に手を動かしてみたところ私の理解が全然できていないことが判明したのでメモしておく。  

#

クロージャーが変数をキャプチャすると言うのは知っていて、普段それを意識するタイミングがあるとすれば、循環参照を避けるために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

クロージャーが参照型であることが書かれていた。