RxSwiftで循環参照してるかもしれない書き方


RxSwiftというよりは、Swiftの話です。

そもそもSwiftの機能として、クロージャの引数とマッチするパラメーターを持つ関数は、 rxSwift.bind(onNext: multiply) のようにクロージャに引数として渡して使えるようなのですが、「これ、簡潔に書けるけど、循環参照って起こったりしないの?」と思ったので検証しました。

こちら参考にさせていただきました。
RxSwiftでクロージャも無いのにメモリリークさせてた

ソースコード

RxSwiftっぽい感じにして簡単に検証しました。

source.swift
import Foundation

class ViewController {

    var intValue: Int
    let rxSwift = RxSwift()

    init(initialValue: Int) {
        self.intValue = initialValue
        print("ViewController init")
    }

    deinit {
        print("ViewController deinit")
    }

    func multiply(by: Int) {
        intValue *= by

        print("value is \(intValue)")
    }

    func multiplyNothing(by: Int) {
        // Do nothing
    }

    func start() {
        rxSwift.bind(onNext: multiply)
    }

    func startNothing() {
        rxSwift.bind(onNext: multiplyNothing)
    }

    func startWeakRef() {
        rxSwift.bind(onNext: { [weak self] value in
            self?.multiply(by: value)
        })
    }

    func startWeakRefDirect() {
        rxSwift.bind(onNext: { [weak self] value in
            self?.intValue *= value
            print("value is \(self?.intValue)")
        })
    }

}

class RxSwift {

    var _onNext: ((Int) -> Void)?
    private var _value: Int = 0
    var value: Int {

        get {
            return _value
        }

        set (newValue){
            _value = newValue
            _onNext?(_value)
        }

    }

    init() {
        print("RxSwift init")
    }

    deinit {
        print("RxSwift deinit")
    }

    func bind(onNext: @escaping ((Int) -> Void)) {
        _onNext = onNext
    }

}

検証1. 「内部でselfを参照する関数をクロージャの引数に渡す」

strong-ref.swift
var vc: ViewController? = ViewController(initialValue: 3)
vc?.start()
vc?.rxSwift.value = 5
vc = nil

// RxSwift init
// ViewController init
// value is 15

循環参照する

検証2. 「selfをキャプチャするクロージャをweak selfで実行する」

weak-ref-direct.swift

var vc: ViewController? = ViewController(initialValue: 3)
vc?.startWeakRefDirect()
vc?.rxSwift.value = 5
vc = nil

// RxSwift init
// ViewController init
// value is 15
// ViewController deinit
// RxSwift deinit

循環参照しない

検証3. 「内部でselfを参照する関数をクロージャの引数に渡す」

weak-ref.swift

var vc: ViewController? = ViewController(initialValue: 3)
vc?.startWeakRef()
vc?.rxSwift.value = 5
vc = nil

// RxSwift init
// ViewController init
// value is 15
// ViewController deinit
// RxSwift deinit

循環参照しない

検証4. 「内部でselfを参照しない関数をクロージャの引数に渡す」

start-nothing.swift

var vc: ViewController? = ViewController(initialValue: 3)
vc?.startNothing()
vc?.rxSwift.value = 5
vc = nil

// RxSwift init
// ViewController init

循環参照する

なぜか

selfがキャプチャされるから。(当たり前)

普段コンパイラに頼りきっていて、「[weak self]をつけろって言われない書き方出来てるし、これで大丈夫やろ!」と思ってしまいそうになりますが、@escaping かどうかはちゃんと見るようにしましょう(自戒

本家のbind(onNext:)

ちゃんと@escaping書かれてて強参照が明示的になってますね。

bind.swift
/**
        Subscribes an element handler to an observable sequence. 

        In case error occurs in debug mode, `fatalError` will be raised.
        In case error occurs in release mode, `error` will be logged.

        - parameter onNext: Action to invoke for each element in the observable sequence.
        - returns: Subscription object used to unsubscribe from the observable sequence.
        */
    public func bind(onNext: @escaping (Self.E) -> Swift.Void) -> Disposable

どうすれば良いのか

関数に処理を切り出すにしても、selfを参照するコードは全て[weak self]にしておくと間違い無いです。
([unowned self]でもいいという記事も見かけたので、絶対そうとは言い切りません)