LoginSignup
6
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-08-05

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]でもいいという記事も見かけたので、絶対そうとは言い切りません)

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7