22
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【UniRx】IObservableのHotとColdを完全に理解する

Last updated at Posted at 2021-03-25

はじめに

RxにつきもののHot Coldについて解説します。

HotColdについてはすでに様々な記事で解説がなされているとは思いますが、直感的には理解ができても、本質的な部分については理解できないことがありました。
先日、ようやく納得がいくまで本質部分を理解できましたので、記事にしてまとめます。
一応、他の記事とは違った視点での説明になっていると思います。

ちなみに、Hot変換については別記事で書いていますのでこちらを参照ください。ただし、本記事の知識が前提となっています。

その前にSubject<T>クラスについて

Hot-ColdとSubject<T>クラスには深い関係があります。
そのため、Hot-Coldの説明の前にSubject<T>クラスについてもう一度復習します。

御存知の通り、Subject<T>クラスはIObserver<T>インターフェイスとIObservable<T>インターフェイスを両方実装したクラスです。
したがって、IObserverとしてもIObservableとしても振る舞うことができるという特徴があります。

複数の購読者を管理する機能を持つ

ここで重要なのは、Subjectクラスは、__Subscribeしてきた購読者(つまりIObserver)を内部にリストとして保持する__ということです。

購読者がSubjectクラスのSubscribeメソッドを呼び出すと、Subjectクラスが内部に持つIObserverのコレクション(購読者リスト)に、引数で渡されたIObserverを追加します。

Subjectの役割_Subscribe.png

そして、SubjectクラスのOnNextOnErrorOnCompletedが呼ばれたときに、保持している購読者リストにあるすべての購読者に対して、同様の通知を発行します。

Subjectの役割_OnNext.png

つまり、Subjectクラスとは、__1つのObservableシーケンスを複数の購読先に分配する、マルチキャストするという役割を持っているクラス__なのです。

まとめると、Subjectクラスの特徴は次のとおりとなります。

  • 複数の購読者を管理する機能を有する
  • 1つのObservableシーケンスをすべての購読先にマルチキャストする

このことを踏まえた上で、Hot Coldの説明をします。

HotとCold

Hot Observableとは

__HotなIObservable__とは、__購読者を複数持つことができるIObservable__のことを指します。
そして、HotなIObservableは「1つのObservableシーケンスを、すべての購読者に対してマルチキャストする」という特徴があります。

「複数の購読者を持つ」「1つのObservableシーケンスを複数の購読先にマルチキャストする」と言えばもう__Subjectクラス__しかありませんね。

Hot Observableは「Subject」

実は、Hot Observableとは、Subjectクラスそのものを指します。文字で説明されても意味がわからないと思うので、次のコードを見てください。

public class HotObservableProvider
{
    private Subject<int> subject = new Subject<int>();

    public void Fire()
    {
        subject.OnNext(1);
        subject.OnNext(2);
        subject.OnNext(3);
    }

    //「HotObservable」から取得できるIObservableはSubjectクラスそのものを指す
    public IObservable<int> HotObservable => subject;
}
public class User
{
    public User()
    {
        HotObservableProvider provider = new HotObservableProvider();
        provider.HotObservable //このIObservable<int>はSubjectクラスそのものを指すのでHot
                .Subscribe(n => { /*何らかの処理*/ });
    }
}

このHotObservableProviderクラスが提供するHotObservableプロパティから取得できるIObservable<int>インターフェイスは、その実体がまさにSubject<int>クラスそのものを指しています
このように、実体がSubjectクラスそのものを指すIObservableは、Hotであると考えることができます。

また、実体がSubjectそのものではなくとも、__Subscribeしたときに実質的にSubjectSubscribeしたのと同じような効果があるようなIObservable__も、Hotと見做すことができます。これについても具体例を用意しました。

//IObservableの実体はSubjectクラスではない
public class HotObservable : IObservable<int>
{
    private Subject<int> subject = new Subject<int>();

    public void Fire()
    {
        subject.OnNext(1);
        subject.OnNext(2);
        subject.OnNext(3);
    }

    //Subscribeしたとき、SubjectクラスをSubscribeするのと同じ効果がなる
    public IDisposable Subscribe(IObserver<int> observer)
    {
        return subject.Subscribe(observer);
    }
}
public class User
{
    public User()
    {
        IObservable<int> observable = new HotObservable();
        //「observable」の実体はSubjectではないが、
        // Subscribeすると実質SubjectをSubscribeしてるのと同じ
        observable.Subscribe(n => { /*何らかの処理*/ });
    }
}

先ほどの例と違って、HotObservableクラス自体がIObservableとなっています。UserHotObservableクラスをIObservableとして使用するとき、当然このIObservableの実体はHotObservableでありSubjectではありません。しかしながら、HotObservableクラスは、Subscribeすると内部に持つSubjectSubscribeするのと同じ効果となるように実装されています。したがって、この場合もHot Observableであると見做すことができます。

まとめると、Hot Observableは、__購読先がSubjectであるようなIObservable__とも言えると思います。

Cold Observableとは

対して__ColdなIObservable__とは、__購読者を1つしか持つことができないIObservable__のことを指します。

ColdなIObservableは購読者を1つしか持てないので、1つの購読者に対して常に専属のIObservableが存在するということになります。HotなIObservableは__Observable:Observer = 1:多であったのに対して、ColdなIObservableObservable:Observer = 1:1__となることが特徴です。

なぜCold Observableが「購読者を1つしか持てない」のかというと、__Subjectクラスを利用していないから__です。Subjectクラスを利用していないので、複数の購読者を管理することができず、ただ1つの購読者に対して専属のObservableが必要になってしまいます。
1つのObservableシーケンスを共有しない(つまり購読者が1つだけ)ならばColdのままで問題ないのですが、共有したい(つまり購読者が複数存在する)場合はHot変換が必要になる場合がある、というのはこのためです。

ちなみに、Rxのオペレータやファクトリメソッドから出てくるIObservableは、そのほとんどがSubjectを使っていません。そのため、普通にRxを使っている分には、ほとんどがColdなIObservableになります。

まとめると…

  • Hot Observable
    • 購読先がSubjectであるようなIObservable
    • 複数の購読者を管理可能
    • Observable:Observer = 1:多
  • Cold Observable
    • 購読先がSubjectではないようなIObservable
    • ただ1つの購読者しか管理できない
    • Observable:Observer = 1:1

Observableソースのお話

これで終わりではなく、大事な__Observableソース__についてのお話があります。

Observableソースとは、__Observableシーケンスに流れてくる値の「発生源」__です。
そしてこのObservableソースにも__Hot__と__Cold__が存在し、この違いを認識していないと思わぬバグの原因となりますので、Rxを使う上では必ず理解する必要がある知識です。

Observableソースの例

ObservableソースはObservableシーケンスに流れてくる値の発生源などと言われてもいまいちピンとこないと思いますので、例を紹介します。

Subject<T>がObservableソース

Subject<T>クラスがObservableソースとなる例です。


Subject<int> subject = new Subject<int>();

subject.Subscribe(n => { /*何らかの処理*/ });

//ここでSubjectにOnNextすることによってObservableシーケンスに値が乗る
//→SubjectがObservableソース
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);

Subjectに対して直接値をOnNextによって送出しています。この場合、Observableシーケンスを流れる値の発生源はこのOnNextにあるので、ObservableソースはSubjectであることが言えます。

ファクトリメソッドがObservableソース

続いて、ファクトリメソッドがObservableソースとなる例です。

//ファクトリメソッドから値が送出されている
//→ファクトリメソッドがObservableソース
Observable.Range(1, 10)
          .Subscribe(n => { /*何らかの処理*/ });

この場合、Observable.RangeからObservableシーケンスの値が送出されていることは言うまでもなく、Observableソースはこのファクトリメソッドということが分かります。

ObservableソースのHotとCold

ObservableソースにもHotとColdがあると書きました。
察しが良い方は勘付くかもしれませんが、上の例で紹介した__Subject<T>がObservableソースのものがHot__で、__ファクトリメソッドがObservableソースのものはCold__です。理由は、Subject<T>は複数の購読者を管理できるのでHot、ファクトリメソッドはSubject<T>を使っていない1ので複数の購読者を管理できずCold、ただそれだけです。

では、ObservableソースのHotとColdが違うとどう影響してくるのかを説明します。

HotなObservableソース

HotなObservableソースとは、__Subject<T>が発生源となるObservableソース__のことを指します。

HotなObservableソースには次のような特徴があります。

  • 何度Subscribeしても同一のObservableシーケンス
  • シーケンスの途中からSubscribeした人には途中からしか流れてこない

Hot Observableソース.png

これはまぁ、想像通りというか、詳しい説明はいらないと思います。
ところが、次のColdなObservableソースの場合、注意が必要です。

ColdなObservableソース

ColdなObservableソースとは、HotではないすべてのObservableソースを指します。
Rxに用意されているファクトリメソッドから作られるIObservableは、そのほとんどが内部でSubject<T>を使用していないため、ColdなObservableソースとして動作します。

ColdなObservableソースには次のような特徴があります。

  • Subscribeするたびに新しいObservableシーケンス
  • __購読者一人ひとりに対して専用のObservableシーケンス__なので、途中からという概念が存在しない

HotなObservableソースと決定的に違う点は、下図にように「Subscribeするたびに新しいObservableシーケンスが生成される」という点です。

Cold Observableソース.png

Hotの場合と違って、ColdなObservableソースはSubscribeするたびに、新しくObservableシーケンスが生成されています。一体なぜこのような動作になるのでしょうか?

答えは、「内部にSubjectを使っていないから」となります。Subjectを使わないということは、購読者に対して「直接」値を発行することを意味します。ちょっと何を言っているのかわからないと思うので、コードで説明します。ここでは、Observable.Rangeファクトリメソッドの動作を簡易的に実装した独自のMyObservable.MyRangeファクトリメソッドを作成してみました。

public static class MyObservable
{
    public static IObservable<int> MyRange(int start, int end) =>
        new MyRange(start, end);
}
class MyRange : IObservable<int>
{
    private int start, end;
    public MyRange(int start, int end)
    {
        this.start = start;
        this.end = end;
    }

    public IDisposable Subscribe(IObserver<int> observer)
    {
        //Subscribeしてくれた人に直接値を送出する
        for (int i = start; i <= end; i++)
        {
            observer.OnNext(i);
        }
        observer.OnCompleted();
        return new Subscription();
    }

    class Subscription : IDisposable
    {
        public void Dispose()
        {
            //SubscribeしたらすぐOnCompletedになるので何もしない
        }
    }
}

Subjectを使わずに値を発行するには、Subscribeの引数に渡されたIObserver(購読者)に対して__直接OnNextするしかない__のです。必然的に、このObservableシーケンスは、__購読してきたIObserver専用__となってしまうのです。したがって、複数回Subscribeされれば、その分新しくObservableシーケンスを生成せざるを得なくなります。これが、上記の図の説明となります。

そして当然、このような仕様を理解していないと、想定とはまったく異なる動作をしてしまう危険性があります。例えば、以下のように1つのObservable.Intervalを時間差で複数回Subscribeした場合。

//0.1秒ごとに0,1,2,…とインクリメントする値を発行するColdなObservableソース
IObservable<long> observableTimer = Observable.Interval(TimeSpan.FromSeconds(0.1));

observableTimer.Subscribe(n => Debug.Log(n));
await Task.Delay(1000); //1秒差でColdなObservableソースを2回購読
observableTimer.Subscribe(n => Debug.Log(n));

「同じ変数observableTimerを2回Subscribeしてるんだから、Observableシーケンスは同じで、2個めのSubscribeには10,11,12,…って値が来るんでしょ」と思っていたら、__大間違い__です。
Observable.IntervalはColdなObservableソースなので、Subscribeするたびに新しく専用のObservableシーケンスが生成されるので、両方とも0,1,2,…と値が発行されます。
もし、前者のような動作をしてほしい場合はHot変換という作業が必要になります。

このように、ObservableソースのHot-Coldを意識することは非常に重要で、これらの違いを理解しないままRxを使い込むと、バグの原因となりますので十分に注意が必要です。

まとめると…

  • Subjectから発生するObservableソースはHot
  • ファクトリメソッドを始めとした、Subject以外から発生するObservableソースはCold
  • ColdなObservableソースは、Subscribeするたびに新しく専用のObservableシーケンスが生成される
  • 想定する動作に合わせて適切にHot変換が必要

さいごに

この記事では、IObservableのHot-ColdとObservableソースのHot-Coldを分けて記述しましたが、本質的には(おそらく)同じで、Subjectを使っているか、使っていないか、それだけの違いになります。
ただその小さな違いが、大きな違い(バグ)を生み出す原因になるので、十分に注意してRxを使うことが必要です。

ご意見ご指摘等ありましたら是非コメント宜しくお願いします!

[2021/4/2追記]
Hot変換の記事を書きましたので、ぜひこちらも参照ください!

参考記事

  1. 一部例外もあるようです。詳しくは@acple@githubさんのこちらの記事を参照ください。

22
10
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
22
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?