LoginSignup
2
2

More than 5 years have passed since last update.

UniRxはSubscribeされるまではオペレータは評価されないし勝手に死ぬ

Last updated at Posted at 2019-03-16

UniRxの寿命管理はしっかりしているらしく、UpdateAsObservable()とかはとても便利で、基本的にMonoBehaviorGameObjectとと寿命を共にすることが多いので、MonoBehaviorのコンポーネントが死ぬときにはGameObjectと共にあるUpdateAsObservable()も根こそぎ死んでくれます。

ですが、今回はMonoBehaviorでないクラスにContextとしてGameObjectを与え、自分自身で寿命管理をさせたくなりました。すると、当たり前のように次のように書いていましたが

this.inputStream = gameObject.UpdateAsObservable()
    .Select(_=> Input.GetKeyDown(KeyCode.A));

// その後、this.inputStream を使ったり使わなかったりする

あれ?寿命大丈夫かな?と不安になってきました。
LinqはCount()とかToList()しないとそこまでのオペレータも評価されないですし、Rxもそうだというようなことを聞いてはいましたが、ちょっと確かめてみないと気が済まなくなったので、順を追ってみていきたいと思います。

引用・参考 neuecc/UniRx
※ コードや表現はUniRxのコードを引用していますが、かなり簡略化しています。マサカリは歓迎ですが、あんまり苛めないでね!

コードトレース

Subjectに何もオペレータがついてない状態でOnNext()されたとき

class Subject
{
    IObserver<T> outObserver = EmptyObserver<T>.Instance;

    // コンストラクタは無い

    public void OnNext(T value)
    {
        outObserver.OnNext(value);
    }
}

EmptyObserverに伝わるので何も起きない。

Subject.Select(selector)のとき

public static IObservable<TR> Select<T, TR>(this IObservable<T> source, Func<T, TR> selector)
{
    return new SelectObservable<T, TR>(source, selector);
}

class SelectObservable
{
    public SelectObservable(IObservable<T> source, Func<T, TR> selector)
        : base(source.IsRequiredSubscribeOnCurrentThread())
    {
        this.source = source;
        this.selector = selector;
    }
}

Subject.outObserverEmptyObserverのままなので、やはり何も起きない。(selectorは動かない)
Subject.Subscribe(observer)も呼ばれていないので、Subjectのインスタンス自体が基本的に変化していないのである。

Subject.Select(selector)のときにSubject.OnNext()が呼ばれたとき

Select(selector)したときのように、そもそもSubject.outObserverが変化していないので、selectorはやはり呼ばれない。

Subject.Select(selector).Subscribe(observer)されたとき

class SelectObservable
{
    protected override IDisposable SubscribeCore(IObserver<TR> observer, IDisposable cancel)
    {
        // sourceはここではSubjectのインスタンス
        return source.Subscribe(new Select_(this, observer, cancel));
    }
}

class Subject
{
    IObserver<T> outObserver = EmptyObserver<T>.Instance;

    public IDisposable Subscribe(IObserver<T> observer)
    {
        // 本当はもっとごちゃごちゃしてる
        outObserver = listObserver.Add(observer);
        return new Subscription(this, observer);
    }
}

ここで初めてSubjectインスタンスに変化が起こり、OnNext()が伝播し始める(selectorを通るようになる)

Subscription.Dispoese()したら?

class Subscription : IDisposable
{
    Subject<T> parent;
    IObserver<T> unsubscribeTarget;

    public Subscription(Subject<T> parent, IObserver<T> unsubscribeTarget)
    {
        this.parent = parent;
        this.unsubscribeTarget = unsubscribeTarget;
    }

    public void Dispose()
    {
        // ここでのペアレントはSubjectのインスタンス
        parent.outObserver = listObserver.Remove(unsubscribeTarget);
    }
}

Subscribeのときの完全な逆操作ですね。

寿命についての結論

やはり、Select()は新しいオブジェクトを生成して返しており、Subjectに参照を渡してはいません。つまりSelect()の返り値への参照がなくなれば勝手にGCされます。Subscribeした時だけは、OnCompleteするまで返り値のSubscriptionを握って適切にDispoese()する必要があります。

時間の都合でそこまで読んでいませんが、恐らくWhere()などのオペレータも同じだと思います。ただ、Buffer()系のは実質的に一旦Subscribe()して新しいStreamを返していそうですね。(要確認)しかしこれも、以上の流儀に従うならば、Subscribe()されていときしかSubjectに参照を渡していなさそうに思います。

2
2
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
2
2