Objective-C
iPhone
iOS
help
Swift

Swiftのクロージャにおける循環参照問題でunownedとweakの使い分けがわからない

More than 3 years have passed since last update.

追記について

追記が増えたので以下の記事を新たに作成しました。こちらと合わせてご参照ください。

Swiftの循環参照問題におけるunownedとweakの使い分けについて

はじめに

クロージャーは定義したスコープの定数や変数をキャプチャすることができます。このキャプチャのおかげでインスタンス変数を定義して、値を保持する必要がなくケースがあるので便利に活用しています。ただキャプチャ値を利用する場合は、循環参照を考慮したコードを記述する必要があります。循環参照に陥ると双方のインスタンスがメモリ上から開放されず、メモリリークの原因になります。

クロージャの循環参照と回避方法

クロージャが循環参照をしている例です。

selfがクロージャを強参照し、クロージャがselfを強参照して循環参照に陥っています。

  • HTMLElementインスタンスはasHTMLプロパティで() -> Stringクロージャを強参照にてインスタンスを保持している
  • () -> StringクロージャはHTMLElementインスタンスをselfとして強参照している

The_Swift_Programming_Language__Automatic_Reference_Counting.jpg

コードでは以下のようになります。[unowned self]というキャプチャリストを記述することでキャプチャしたselfHTMLElementインスタンス)をクロージャはselfを強参照しなくなります。

強参照を回避するためにはunownedとは別にweakもあります。
unownedweakの違いはキャプチャする変数の値がnilになりえる時(Optional)はweak、値が常に存在する場合はunownedになります。

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [unowned self] in // <- Unowned References
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        println("\(name) is being deinitialized")
    }

}

Appleの公式ドキュメントより Swift Automatic Reference Counting

以下わからないころの紆余曲折な内容です

コメント欄の議論の原点なので残しておきます。

こちらの内容をご参照ください。
Swiftの循環参照問題におけるunownedとweakの使い分けについて

unownedとweakの使い分けがわからない

例えば、UIViewControllerのselfをキャプチャする場合は、unownedweakのどちらを記述すればよいのかよくわかりません。
キャプチャする変数の定義にOptionalが明示的に宣言されていればわかりやすいのですが。

クロージャがUIViewControllerをselfとしてキャプチャしているところをunownedで記述するとクラッシュしてしまいます。weakだとクラッシュしません。selfはUnwrapせずにアクセスするのでOptionalではないと思うのですが、weakを記述することが正しいのでしょうか。

【追記】このクラッシュの事象はBeta6では改善されているようです。Beta5では発生していました。

実行時エラー (Beta6では改善済)

クラッシュすると以下のようなエラーが出ます。

news-simple-app_xcworkspace.jpg

同じ境遇の方も。。。

コード例

BlocksKitを使っています。selfはUIViewControllerのインスタンスです。

/// unowned は クロージャをセット時にクラッシュ
button.bk_addEventHandler({ [unowned self] (sender: AnyObject!) in
    self.navigationController.pushViewController("HOGE", animated: true)    
}, forControlEvents: UIControlEvents.TouchUpInside)

/// weak は OK 
button.bk_addEventHandler({ [weak self] (sender: AnyObject!) in
    if let selfWeak = self {
        selfWeak.navigationController.pushViewController("HOGE", animated: true)
    }
}, forControlEvents: UIControlEvents.TouchUpInside)

【追記】「初期化後にnilである可能性がある」場合には、unownedではなく、weakを使うべき

UIViewControllerのselfにおいては、クロージャ実行時にselfがnilになる可能性があるのでweakを使うのがよさそうです。

UIViewControllerのselfにおいては、どのケースでどちらを利用すればよいか確認中です。
Beta5ではunownedを利用するとクラッシュしていたので、weakを使うべきと判断していたのですが、Beta6ではクラッシュが改善されておりunownedも記述はできるためです。

判断基準について

コメント欄にて、@tomohisaota さんよりご教授頂きました。ありがとうございます。
@tomohisaota さんのQiita投稿でdelegateの場合ですが、unownedweakについて丁寧に解説してくださっているのでそちらも合わせてご参照ください。

参考1 @tomohisaota さんのコメントより

弱参照は常にweakをつかって、nilになる可能性を意識して書けばOKです。
unownedとweakの最大の違いは、オブジェクト破棄時にnilクリアされるかどうかです。
unownedはnilクリアされないので、オブジェクト破棄後にアクセスすると落ちます。
weakはnilクリアされるので、オブジェクト破棄後にアクセスしてもnilになります。

unownedを使うのは、オブジェクトの生成タイミングをきっちり把握していて、絶対にブロック実行時にインスタンスが破棄されていないことが確信できる場合のみです。
その場合であっても、weak使ってはいけないわけじゃないので、迷うならweak一択です。

参考2 @tomohisaota さんのQiita投稿

なぜdelegateはunowned(unsafe)+Implicitly Unwrappedなの?

そもそも「初期化後にnilである可能性がある」場合には、unownedではなく、weakを使うべきです。

キャプチャリストを記述すべきかわからない

【追記】プロパティのget, set, willSet, didSetはクロジャーではないので、キャプチャリストは不要です。

プロパティのget, set, willSet, didSetで参照するselfはキャプチャリストが必要かわからないです。

override var description: String! {
    get {
        return "\(self.title) \(self.url)"
    }
}


var title: String! {
    didSet {
        println(self.description)       
    }
}

unownedの場合のエラー

WTDArticleView_swift_—_Edited.jpg

weakの場合のエラー

WTDArticleView_swift.jpg

他にも困っている方が。。。

実行環境

  • Xcode6 Beta5
  • iOS7.1 iPhone5 実機
  • iOS8.0 Beta5 iPhone5s シミュレータ
  • iOS8.0 Beta5 iPhone5 シミュレータ

参考資料