Closureと[weak self]を理解する


はじめに

RxSwift便利ですよね。自称何でもリアクティブに書きたい芸人です。ただ、RxSwiftを学ぶときに[weak self]で痛い思いをしたのは私だけではないはず…!!Qiitaや他のブログとかでこの[weak self]の話を見かけることが時々あり、その度に「あるあるw」と思っています。

しかし、その解説でなんだか結論が
「Closure内では[weak self]をつけましょう」
と言ってる記事が多すぎる気がしました。

こういう記事を見かける度にいつも、「うーん、でもforEachとかの引数もClosureだよなー…。あれもつけるべきなのかなー」と考えてしまって悩んでいたので、その辺を調べたものをまとめます。

やってみた

forEachのClosure内で[weak self]を試しみました。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        array.forEach({[weak self] str in
            self?.string = str
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

案の定ですが、このViewControllerはpopViewControllerされると破棄されました。では、[weak self]を取り除いてみます。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        array.forEach({ str in
            self.string = str
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

このViewControllerをpopViewControllerしてみたところ、なんと破棄されていました!

「うーん、、、???」となっていましたが、別のClosureを試してみるとなんとなく納得できる結果を得られました。それは、DispatchQueue.main.asyncAfterです。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
            self.string = "aaa"
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

このViewControllerに遷移し1秒くらいでpopViewControllerしたところ、少し経ってから破棄されるのが確認されました(たぶんpopから9秒後)。また、これに[weak self]をつけるのも試してみました。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {[weak self] in
            self?.string = "aaa"
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

結果は、popViewController時にすぐに破棄されていました。
(※ついでに[weak self]ではなく[unowned self]を試すと、10秒後にfatal errorが発生しました。)

わかったこと

標準で提供されているClosureを引数にとる関数は、その関数が必要なくなったタイミングで破棄されているのだと思いました。forEachは、その処理が終わったタイミングでClosureが破棄されClosure→Selfへの参照がなくなっていたため、popViewControllerのときに参照カウントが0になり破棄されたんだと思います。asyncAfterは、popViewController時はClosureがまだ生きておりClosure→selfの参照が残っていたのでViewControllerが破棄されず、Closureが実行され処理が終わったタイミングでClosureが破棄されselfの参照カウントが0になったので破棄されたんだと思います。

つまり、標準提供されている関数はClosure実行終了時にClosureが破棄され循環参照が解除されるので、[weak self]を付ける必要がないことがわかりました。

RxSwiftではなぜ[weak self]をつけるのか

ここまで調べてまた一つ疑問が出ました。
「RxSwiftでもClosure実行されたら破棄すればよくない?」
そう思ってRxSwiftのコードを追ってみましたが、明確な理由はわかりませんでした(難しすぎた…orz)。
ただおそらく以下のような理由だと考えています。RxSwiftは任意のタイミングでイベントを発行できます。ですので、何度も同じ処理を作っては破棄して作っては破棄して、を繰り返すよりは一つのインスタンス的な扱いで保持しておくほうがパフォーマンスが良かったのかと思います。その代わり、もう処理をしなくなったタイミングで破棄するために明確に宣言する必要がでてきます。それがRxSwiftのdisposeメソッドだと考えました。そのため、以下の実装を試してみました。

import UIKit
import RxSwift

class ViewController: UIViewController {
    let publishSubject = PublishSubject<String>()
    var eventString = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        publishSubject.subscribe(onNext: { str in
            self.selfString = str
        })
        publishSubject.dispose()
    }

    deinit {
        print("破棄されたよー")
    }
}

RxSwiftでは有名な、Closure内の[weak self]をつけ忘れているコードです。ですが、このViewControllerをpopViewControllerしてみると、正しく破棄されたのが確認されました!つまり、dispose()メソッドを呼べばClosureが破棄され循環参照が解除されたため、破棄できるようになったということがわかりました。

import UIKit
import RxSwift

class ViewController: UIViewController {
    let publishSubject = PublishSubject<String>()
    var eventString = ""
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        publishSubject.subscribe(onNext: { str in
            self.selfString = str
        }).disposed(by: disposeBag)
    }

    deinit {
        print("破棄されたよー")
    }
}

よく見かける上記のコードですが、これはdisposeBagが破棄されたタイミングでdisposeメソッドを呼ぶように書かれています。しかしselfが破棄されないとdisposeBagも破棄されないため、永遠にdisposeメソッドは呼ばれず循環参照が続くコードになっていました。

まとめ

  • 標準提供されている関数では、Closureは処理が終わったタイミングで破棄されるため[weak self]をする必要がない
  • RxSwiftでは、Closureは処理が終わっても破棄されないため[weak self]で循環参照を防いでおく必要がある
  • RxSwiftではClosureを破棄する手段としてdispose()が提供されているため、それを正しく呼べれば手動で循環参照を解除することもできる

RxSwift内のClosureだけ気をつけておけばとりあえずは良さそうで、安心しました。