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