Unity
UniRx

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

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に参照を渡していなさそうに思います。