なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

【UniRx】IObserver, IObservable, ISubjectについて

始めに

UniRxを勉強すると必ず3つのインターフェースIObserver, IObservable, ISubjectが登場します。

勉強仕立てのときはこれらの関係性どころか覚えることすらできませんでした。

今回はその反省を踏まえ、自分なりにこれら3つをまとめていこうと思います。


また、今回の内容は前回のUniRxいらすとやの続きになります。

shitakami.hatenablog.com



IObservableについて

こちらは前回ポストだとお話したものになります。

f:id:vxd-naoshi-19961205-maro:20210516135812p:plain


次に、IObservableは次のように定義されています。

ちなみにSubscribeは「購読する」と訳されます。

public interface IObservable<T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

見たらわかるかもしれませんが、実はメッセージを送るなどの処理は宣言されていません。

IObservableが行うことはobserver(仕事をする人)を登録するだけとなります。

f:id:vxd-naoshi-19961205-maro:20210602233015p:plain



IObserverについて

こちらはObservableに何かしらが入ったときにそれを受け取ってその内容を処理するものと書きました。完結に言ったら仕事をする人と表せるかもしれません。

f:id:vxd-naoshi-19961205-maro:20210516135822p:plain


これは次のように定義されています。

public interface IObserver<T>
{
    void OnNext(T value);
    void OnError(Exception error);
    void OnCompleted();
}

実はIObserverは3種類のメッセージを受け取ることができ、それぞれに対する仕事を行えます。

  • OnNext : T型の値が来た際に何かしらの処理をする。基本的にはこれがメイン。
  • OnError : Observable等でエラーが起きた際にExceptionを受け取り、エラー処理を行う。
  • OnCompleted : 完了処理を行う。これを実行した後はデータが来ても仕事はしない。



ISubject

ISubjectはIObservableとIObserverの2つを実装したものとなります。

これは自分でメッセージを送ることが出来るObervableと見ていいでしょう。

ISubjectは次のように定義されています。

public interface ISubject<TSource, TResult> : IObserver<TSource>, IObservable<TResult>
{
}

public interface ISubject<T> : ISubject<T, T>, IObserver<T>, IObervable<T>
{
}


普段はこのISubjectを使う機会はあまりないですが、Subjectクラスをよく目にします。

このSubjectクラスを使うことで、複数のIObserverを購読してそれらにメッセージを送れます。

f:id:vxd-naoshi-19961205-maro:20210602233117p:plain


3つを簡単に使ってみる

今回はこれらを使って簡単なサンプルを作ってみようと思います。

始めにSomeInputクラスを作成しました。こちらはSubjectを生成して、スペースキーを押すたびに再生してからの経過時間を通知します。

外部にはSubjectをIObservableとして公開します。外部からSubscribeするときはこのIObservableを使います。

using UnityEngine;
using UniRx;
using System;

public class SomeInpute : MonoBehaviour
{

    private Subject<float> subject = new Subject<float>();

    public IObservable<float> TimeObservable => subject;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            subject.OnNext(Time.time);
        
    }
}


次にMessageDisplayerクラスを作成します。こちらはSomeInputクラスのObservableを購読します。 クラス内にIObserverを実装したValueDisplayObserverを定義し、Observableからのメッセージを実行させます。

using System;
using UniRx;
using UnityEngine;

public class MessageDisplayer : MonoBehaviour
{
    private class ValueDisplayObserver : IObserver<float>
    {
        public void OnNext(float value)
        {
            Debug.Log(value);
        }

        public void OnError(Exception error)
        {
            Debug.LogError(error);
        }

        public void OnCompleted()
        {
            Debug.Log("Message Complete");
        }
    }

    [SerializeField]
    private SomeInpute _someInpute;

    private void Start()
    {
        IObserver<float> observer = new ValueDisplayObserver();
        _someInpute.TimeObservable
            .Subscribe(observer)
            .AddTo(this);
    }

}


この2つのクラスを図にするとこんな感じになります。  

f:id:vxd-naoshi-19961205-maro:20210604000039p:plain

SomeInputの中ではSubjectが生成されており、外部からはIObservableとして扱うことが出来ます。

なので、SomeInput内ではメッセージを通知することが出来ますが外部からは購読のみしかできません。このSomeInputが公開したIObservableを使ってMessageDisplayerで購読を行います。

Subjectに経過時間を渡すと、購読したMessageDisplayerのObserverにその経過時間が通知されて画面に表示する処理を行います。



Subscribeで匿名関数を渡す

先ほど作成したサンプルではいちいちIObserverを実装したクラスを作成していますが、IObservableの拡張メソッドでは匿名関数をデリゲートに渡すことが可能です。

githubのUniRxのリポジトリからの引用です。(neuecc/UniRx

        public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext)
        {
            return source.Subscribe(Observer.CreateSubscribeObserver(onNext, Stubs.Throw, Stubs.Nop));
        }

        public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action<Exception> onError)
        {
            return source.Subscribe(Observer.CreateSubscribeObserver(onNext, onError, Stubs.Nop));
        }

        public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action onCompleted)
        {
            return source.Subscribe(Observer.CreateSubscribeObserver(onNext, Stubs.Throw, onCompleted));
        }

        public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action<Exception> onError, Action onCompleted)
        {
            return source.Subscribe(Observer.CreateSubscribeObserver(onNext, onError, onCompleted));
        }


これらの拡張メソッドを使って既存のメソッドやラムダ式で書かれた匿名関数を引数のデリゲートに渡すことができます。 また、OnErrorやOnCompletedが必要なければ省くことも可能です。

_someInpute.TimeObservable
    .Subscribe(time => Debug.Log(time)) // OnNextだけを指定
    .AddTo(this);



最後に

今回はざっくりと3つのインターフェースについてまとめてみました。

前回と同じように個人的なイメージを固めるために書いたのでそこまで深い内容ではないと思います。

次回は全く触れなかったAddToや購読の停止関係をまとめていこうと考えています。



参考

qiita.com

qiita.com



追記 : デリゲート, 匿名関数, ラムダ式について

ブログを投稿したあとに、「ラムダ式を渡す」という表現はおかしいとお教えいただいたので今一度デリゲート, 匿名関数, ラムダ式について復習しようと思います。

本記事とはあまり関係ないので興味ない方は無視してください。

デリゲート

デリゲートとはメソッドを参照するオブジェクトです。私の認識としてはC/C++の関数ポインタみたいなものと考えていました。

ただし、異なる点として複数のメソッドを参照することができ、デリゲート呼び出しを行うことで参照しているメソッド全てを呼び出せます。(マルチキャストデリゲート)

また、C#が用意しているジェネリック型のデリゲートとして次の2つがあります。

  • System.Action<T1, T2, . . .> : 返り値なしのデリゲート、引数は指定可能(なしも可)
  • System.Func<T1, T2, . . . , TResult> : 返り値ありのデリゲート、型パラメーターの最後に返り値の型を指定


匿名関数

Microsoftのドキュメントでは次のように定義されています。

匿名関数は、デリゲート型が必要とされる任意の場所で使用できる "インライン" のステートメントまたは式です。


また、こちらの記事ではこのようにも書かれています。(Microsoftドキュメントの"C#のデリゲートの進化"にも同じことが書かれています。)

C# 2.0では匿名メソッド式、C# 3.0ではラムダ式という構文が入り、これらを合わせて匿名関数と呼びます。

よって、匿名メソッドやラムダ式の総称を匿名関数としているようです。


匿名メソッド、ラムダ式

匿名メソッドはC#2.0で追加された機能で次のような書き方を指すようです。 (公式ドキュメントより引用

        TestDelegate testDelB = delegate(string s) { Console.WriteLine(s); };

書き方としてはdelegate(型 引数, . . .) { /* 何かしらの処理 */ }となるようです。ただ、ドキュメントでは匿名メソッドよりもラムダ式を使うことを進めています。


ラムダ式C#3.0で追加された機能で、匿名メソッドをより簡潔にかけるもので式形式のラムダとステートメント形式があるそうです。

// 式形式のラムダ
Func<int, int> square = x => x * x;   

// ステートメント形式のラムダ
Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};

このようにラムダ式はデリゲート型のActionやFuncに変換することができます。


参考

delegate 演算子 - C# リファレンス | Microsoft Docs

匿名関数 - C# プログラミング ガイド | Microsoft Docs

ラムダ式 - C# リファレンス | Microsoft Docs

ローカル関数と匿名関数 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

ラムダ式 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C