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
でも問題ないと考えています。
非同期処理などでは、キャプチャ先のインスタンスの生存期間を意識して、適切に使い分けられている状態が一番健全だと思います。
まとめ
キャプチャする際は、適切なものを使いましょう。