RxSwiftで引数付きの関数を追加 (extension) する場合は気をつけるという話


はじめに

VALU Advent Calendar 2018 の2日目です!
1日目のAndroidに続き,本日はiOS向けのエントリです。

RxSwift は便利ですが,extensionで引数付きの関数を利用する際には気をつけたい注意点があります。今回は RxSwift によって問題が発生しうるコードを例に,その解決策を紹介致します。

問題が発生しうるコード

extension Observable where E == Int {
    func add(_ value: Int) -> Observable<Int> {
        return map { $0 + value }
    }
}

以上は Observable の中身が Int であるものに対し,value を加えるという単純な add() 関数です。
一見通常の extension なのですが,以下の場合に実行してみるとそれが好ましくない挙動をすることが分かります。

let subject = PublishSubject<Int>()
var value = 10

_ = subject
    .add(value)
    .subscribe(onNext: { print($0) })

value = 5
subject.onNext(0) // 10

本来 5 で定義されて欲しい value が,10 と表示されてしまいます。

どうすれば良いのか

遅延評価させれば良いのですが,Swift には遅延評価のための lazy は class および collection (Array) に対してのみ開放されており,利用できません。
Closure を利用することになります。
ただし,関数内の map に Closure を利用しているため,@escaping キーワードを利用する必要があり,呼び出しはクロージャにする必要があります。

extension Observable where E == Int {
    func add(_ value: @escaping () -> Int) -> Observable<Int> {
        return map { $0 + value() }
    }
}
_ = subject
    .add({ value })
    .subscribe(onNext: { print($0) })

より良い方法

本題です。以上の方法で解決自体はできるのですが,やはり add({ value }) の Closure が気になります。
Swift の値渡しと参照渡しのように,利用する側としては素朴な概念と一致する現在の値が渡って欲しいと考えます。そうしましょう。

@autoclosure キーワードを利用します。

extension Observable where E == Int {
    func add(_ value: @escaping @autoclosure () -> Int) -> Observable<Int> {
        return map { $0 + value() }
    }
}
let subject = PublishSubject<Int>()
var value = 10

_ = subject
    .add(value)
    .subscribe(onNext: { print($0) })

value = 5
subject.onNext(0) // 5
value = 20
subject.onNext(0) // 20

これにより,add(value) 関数内において,呼び出し側を変更することなく安全に記述することが可能となりました。

結語

今回は Rx の引数問題を @autoclosure を利用して解決してみました。

台無しにする結語を加えると,そもそも引数を固定にせず,Single 等を利用して都度取りにいくのが良いでしょう。
UseCase 等を利用していて設計上 疎結合となっているならば,自然とそうなる気がしています。

References