UniRxの寿命管理はしっかりしているらしく、UpdateAsObservable()とかはとても便利で、基本的にMonoBehaviorはGameObjectとと寿命を共にすることが多いので、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.outObserverはEmptyObserverのままなので、やはり何も起きない。(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に参照を渡していなさそうに思います。