DisposeBagに複数まとめて登録する時は`.disposed(by:)`を使うよりも`insert()`を使う方が良い


TL;DR

hoge.subscribe(
    onNext: { ~ },
    onError: { ~ }
)
.disposed(by: disposeBag)
fuga.subscribe(
    onNext: { ~ },
    onError: { ~ }
)
.disposed(by: disposeBag)

disposeBag.insert(
    hoge.subscribe(
        onNext: { ~ },
        onError: { ~ }
    ),
    fuga.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
)

って書こうぜって話

DisposeBagとは

subscribeしているObservable等をまとめてdisposeしてくれる仕組みです。
DisposeBagオブジェクトが破棄されるタイミングで抱えているDisposableに対してdispose()をかけるようになっています。

参考記事はこちら

DisposeBagの実装

ソース
https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Disposables/DisposeBag.swift

まずはdisposed(by:)を見ていきます

DisposeBag.swift
extension Disposable {
    /// Adds `self` to `bag`
    ///
    /// - parameter bag: `DisposeBag` to add `self` to.
    public func disposed(by bag: DisposeBag) {
        bag.insert(self)
    }
}

はい、baginsertしてるだけですねw
ではこのinsertを見ていきます

DisposeBag.swift
private var _lock = SpinLock()

// state
private var _disposables = [Disposable]()
private var _isDisposed = false

/// Adds `disposable` to be disposed when dispose bag is being deinited.
///
/// - parameter disposable: Disposable to add.
public func insert(_ disposable: Disposable) {
    self._insert(disposable)?.dispose()
}

private func _insert(_ disposable: Disposable) -> Disposable? {
    self._lock.lock(); defer { self._lock.unlock() }
    if self._isDisposed {
        return disposable
    }
    self._disposables.append(disposable)
    return nil
}

これを見ると、
- bagが既に破棄されている場合にはinsertされたDisposableも破棄しちゃう
- bagが破棄されていないなら内部のキューに積む

といった動きをしていることが分かります。

また、DisposeBagには複数のDisposableをまとめて処理するためのメソッドも用意されています。

DisposeBag.swift
/// Convenience function allows a list of disposables to be gathered for disposal.
public func insert(_ disposables: Disposable...) {
    self.insert(disposables)
}

/// Convenience function allows an array of disposables to be gathered for disposal.
public func insert(_ disposables: [Disposable]) {
    self._lock.lock(); defer { self._lock.unlock() }
    if self._isDisposed {
        disposables.forEach { $0.dispose() }
    } else {
        self._disposables += disposables
    }
}

こちらも

  • bagが既に破棄されている場合にはinsertされたDisposableも破棄しちゃう
  • bagが破棄されていないなら内部のキューに積む

という点では一緒ですね。

つまり、

hoge.subscribe(
    onNext: { ~ },
    onError: { ~ }
)
.disposed(by: disposeBag)
fuga.subscribe(
    onNext: { ~ },
    onError: { ~ }
)
.disposed(by: disposeBag)

といったコードは

disposeBag.insert(
    hoge.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
)

disposeBag.insert(
    fuga.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
)

と置き換えられ、更に

disposeBag.insert(
    hoge.subscribe(
        onNext: { ~ },
        onError: { ~ }
    ),
    fuga.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
)

と置き換えられることが分かります。

見た目的にもここでまとめてdisposeBagに突っ込んでることが分かりやすくなりますし、
SpinLockについては詳しくないので断言はできませんが、
.disposed(by:)だと一々ロックかけたり解除したりを繰り返すのに対し、
まとめてinsertするなら1回のロックで済むのでパフォーマンス的にも良いんじゃないかなーと思ってます。
(あと、.繋ぎは改行するような規約の現場なら、シンプルに行数が減るのでLinterにも優しくなるはず)

結論

DisposeBag.insert()、積極的に使っていこう!

2021/01/26追記

RxSwift 6がリリースされ、DisposeBag.insertがFunction Builderに対応したようです。

disposeBag.insert {
    hoge.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
    fuga.subscribe(
        onNext: { ~ },
        onError: { ~ }
    )
}

カンマが不要になるので項目が増える際のdiffも減ってレビューもしやすくなりますね。

参考

https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Disposables/DisposeBag.swift
https://dev.to/freak4pc/what-s-new-in-rxswift-6-2nog#new-raw-disposebag-endraw-function-builder