しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

SwiftのARCとクロージャのキャプチャ

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]のように無修飾でインスタンス名だけ書くことで指定できます。

それに加えて、キャプチャする変数の型がクラスか、プロトコルの場合、weakunownedの指定方法があります。

  • 弱参照(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)の場合

unownedweakと同じ弱参照ですが、若干挙動が違います。

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

ここで良く話題にあがる、weakunownedのどちらが良いかという問題を考えてみます。
自分なりの考えは、「参照先が取れることがを前提とした処理ならば、unownedを使う。weakならば取れない場合のハンドルをしっかりするべき」といった感じです。

force unwrapやforce unwrap同様、fatal errorを起こしうる処理を、避ける考えもありますが、ただ単に循環参照を避けるためだけならば、unownedでも問題ないと考えています。
非同期処理などでは、キャプチャ先のインスタンスの生存期間を意識して、適切に使い分けられている状態が一番健全だと思います。

まとめ

キャプチャする際は、適切なものを使いましょう。