Swiftのバージョンが上がる前に、クロージャーに対して各種方法で変数をキャプチャした際の挙動を整理します。
ちなみに、新卒アドベントカレンダーではないです。
ARCについての前提知識
Swiftでは、ARC(Automatic Reference Counting)というメモリ管理方式を用いています。他言語でもよくある参照カウント方式です。
こちらの方式は、インスタンス変数の参照された回数を記憶しておき、
インスタンスが参照(強参照)された時にそれをインクリメントして、ブロックを抜けるなどインスタンスが参照されなくなった際に、デクリメントします。
そして、カウントが0になった際に、割り当てられたメモリ領域を解放します。
詳しくは、公式ドキュメントに書かれています。(長い)
Automatic Reference Counting — The Swift Programming Language(Swift 4.2)
強参照と弱参照の違い
参照には弱参照と呼ばれる、参照カウントを増やさない参照があります。
特に、Swiftではクロージャーでの循環参照を回避する際に用いられることがあります。
強参照
強参照は、参照カウントをインクリメントする参照です。
つまり、参照先のインスタンスの参照カウントによらず、明示的に解放するなど、よほどのことがなければ、自身の分の参照カウント1つ分が残るので、常にインスタンスが生きていることになります。
Swiftでは特に意識せず何も指定しなかった場合、強参照となります。
例えば、letで変数を作成した時は、右辺が参照しているインスタンスの参照カウントが増えます。
let foo = Foo() // 参照カウントが増える let hoge = foo
弱参照
それに対して、弱参照は参照カウントを増やさない参照です。
つまり、参照先の参照カウントが0になることもあり得るので、インスタンスの生存は保障されません。
Swiftの場合、weak varやキャプチャのunownedがそれにあたります。
(C++でいうとweak_ptrです。)
let foo = Foo() // 参照カウントが増えない weak var hoge = foo
キャプチャの種類
変数のキャプチャは指定方法の視点から見ると4種類あります。
- デフォルト(何もつけない)
- 値キャプチャ
デフォルトは、キャプチャ方法を何も指定しない方法です。
値キャプチャは、[hoge]のように無修飾でインスタンス名だけ書くことで指定できます。
それに加えて、キャプチャする変数の型がクラスか、プロトコルの場合、weakとunownedの指定方法があります。
- 弱参照(weak)
- 非所有参照(unowned)
weak, unownedはそれぞれ、[weak hoge]、[unowned hoge]のようにすることで指定できます。
組み込み型の場合
ここでは、String型を例に挙げます。
何もつけない場合
キャプチャの指定を何もしない場合、クロージャー内で使用される変数は、強参照扱いになります。
クロージャーを引数に取るメソッドの例として、DispatchQueue.main.asyncAfterを使います。
func captureString() { var str: String = "Hello!" print(str) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { print(str) } str = "Bye!" }
この場合出力は、以下のようになります。(後の例でわかりやすいようにアドレスを添えてあります。)
<0x00007ffee8e24960> Hello! <0x00007ffee8e25e70> Bye!
こちらは、クロージャ内でstrの強参照を持っているので、中のprintが実行された際には、参照先が"Bye!"に変わっており、出力されるのは"Bye!"になります。
値キャプチャの場合
では、値キャプチャの場合はどうなるのでしょうか。
func captureStringValue() { var str: String = "Hello!" print(str) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [str] in print(str) } str = "Bye!" }
この場合、出力は以下のようになります、
<0x00007ffee2f47950> Hello! <0x00007ffee2f48e68> Hello!
注目すべき点はメモリアドレスが変わっていることです。
これは、キャプチャする際に、deep copyが走るため、中で用いるstrはコピーされた方の"Hello!"を指していることになります。
したがって、後からstrを"Bye!"に変更しても、クロージャー内では影響がないということになります。
クラスインスタンスの場合
では、クラスの場合はどうでしょう。 適当なクラスを用意してみます。
class Foo { let name: String init(_ name: String) { self.name = name } }
無指定、値キャプチャの挙動は、Stringと同じです。
func captureClass() { var foo = Foo("foooo!") print(foo.name) // Strong reference DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { print(foo.name) } foo = Foo("gaaaa!") } func captureClassValue() { var foo = Foo("foooo!") print(foo.name) // Value capture DispatchQueue.main.asyncAfter(deadline: .now() + 0。5) { [foo] in print(foo.name) } foo = Foo("gaaaa!") }
弱参照(weak)の場合
weakを指定した場合どうなるか見てみます。
func captureClassWeak() { var foo = Foo("foooo!") print(foo.name) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak foo] in print(foo?.name) } foo = Foo("gaaaa!") }
<0x00007ffeead4a958> foooo! <0x00007ffeead4be70> nil
weakを指定した場合、キャプチャされた変数はOptionalでラップされます。
そして、弱参照なので参照カウントが増えないため、
インスタンスFoo("foooo!")の参照カウントは、foo = Foo("gaaaa!")で0となります。
つまり、クロージャーの呼び出し時には、すでに参照しているメモリ領域は解放済みなので、クロージャ内のOptional<Foo>のfooにはnilが入るということになります。
弱参照(unowned)の場合
unownedはweakと同じ弱参照ですが、若干挙動が違います。
func captureClassUnowned() { var foo = Foo("foooo!") print(foo.name) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [unowned foo] in print(foo.name) } foo = Foo("gaaaa!") }
<0x00007ffedff69958> foooo! Fatal error: Attempted to read an unowned reference but object 0x6000028d83c0 was already deallocated2018-12-24 12:59:58.179139+0900 SwiftARCCheck[42371:3001004] Fatal error: Attempted to read an unowned reference but object 0x6000028d83c0 was already deallocated
こちらは、weakと違いOptionalでラップされないので、もし参照先が解放されていた場合、
使用しようとするとfatal errorで落ちます。
weak vs unowned
ここで良く話題にあがる、weakとunownedのどちらが良いかという問題を考えてみます。
自分なりの考えは、「参照先が取れることがを前提とした処理ならば、unownedを使う。weakならば取れない場合のハンドルをしっかりするべき」といった感じです。
force unwrapやforce unwrap同様、fatal errorを起こしうる処理を、避ける考えもありますが、ただ単に循環参照を避けるためだけならば、unownedでも問題ないと考えています。
非同期処理などでは、キャプチャ先のインスタンスの生存期間を意識して、適切に使い分けられている状態が一番健全だと思います。
まとめ
キャプチャする際は、適切なものを使いましょう。