はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UniRxのIObservable<T>.Debugメソッドの利用方法と仕組みの考察(デバッグ・テストに便利)

はじめに

今回はUniRxのテスト・デバッグに使えるIObservable<T>.Debugを紹介したいと思います。

f:id:hanaaaaaachiru:20220122181052p:plain
動作例

github.com

概要

IObservable<T>.Debugオペレーターを挟むことで、以下のタイミングでログを出力することができます。

public static IObservable<T> Debug<T>(this IObservable<T> source, string label = null);
public static IObservable<T> Debug<T>(this IObservable<T> source, UniRx.Diagnostics.Logger logger);

ログ出力するのは以下のタイミング*1です。

  • OnNext
  • OnError
  • OnComplete
  • OnCancel
  • OnSubscribe
f:id:hanaaaaaachiru:20220122181052p:plain
出力されている様子

利用サンプル

public class Tests
{
    [Test]
    public void SimplePasses1()
    {
        var subject = new Subject<int>();
        var observable = (IObservable<int>) subject;

        // stringを引数にとったDebugオペレーターを追加する
        observable = observable.Debug("MyDebug");
        
        observable.Subscribe(x => {});                        // [MyDebug]OnSubscribe
        var disposable = observable.Subscribe(x => { });      // [MyDebug]OnSubscribe
        subject.OnNext(1);                                    // [MyDebug]OnNext(1), [MyDebug]OnNext(1)
        subject.OnNext(2);                                    // [MyDebug]OnNext(2), [MyDebug]OnNext(2)
        disposable.Dispose();                                 // [MyDebug]OnCancel
        subject.OnNext(3);                                    // [MyDebug]OnNext(3)
        subject.OnCompleted();                                // [MyDebug]OnCompleted()
    }
    
    [Test]
    public void SimplePasses2()
    {
        var subject = new Subject<int>();
        var observable = (IObservable<int>) subject;

        // stringを引数にとったDebugオペレーターを追加する
        observable = observable.Debug("MyDebug");
        
        observable.Subscribe(x => {});                          // [MyDebug]OnSubscribe
        subject.OnNext(1);                                      // [MyDebug]OnNext(1)
        try
        {
            subject.OnError(new Exception("Hoge Hoge"));        // [MyDebug]OnError(System.Exception)
            Assert.Fail();
        }
        catch
        {
            Assert.Pass();
        }
    }
    
    [Test]
    public void SimplePasses3()
    {
        var subject = new Subject<int>();
        var observable = (IObservable<int>) subject;

        // Loggerを新規作成し、Loggerの出力をUnityEngine.Debug.Logするように購読する
        // デフォルトではUnityEngine.DebugLogの実行を購読していないみたいなので、自身で設定してあげる必要あり(staticのため共通のインスタンスなので注意)
        var logger = new UniRx.Diagnostics.Logger("MyLogger");
        ObservableLogger.Listener.Subscribe(x => Debug.Log($"[{x.LoggerName}]{x.Message}"));
        
        // UniRx.Diagnostics.Loggerを引数にとったDebugオペレーター
        observable = observable.Debug(logger);
        
        observable.Subscribe(x => {});              // [MyLogger]OnSubscribe
        subject.OnNext(1);                          // [MyLogger]OnNext(1)
        subject.OnCompleted();                      // [MyLogger]OnCompleted()
    }
}

コメントにも書いておきましたが、UniRx.Diagnostics.Loggerを利用する場合は購読する処理も書かなければいけません。

ただObservableLogger.Listenerstaticで共通なので、他のLoggerインスタンスの動作にも影響が出てしまいます。
(LoggerのコンストラクタにてObservableLogger.logPublisherOnNextと紐付けれており、Listener.SubscribeメソッドはObservableLogger.logPublisherを購読している)

仕組み

動作の理解のために中のコードを見てみましょう。

namespace UniRx.Diagnostics
{
    public static class ObservableDebugExtensions
    {
        /// <summary>
        /// Debug helper of observbale stream. Works for only DEBUG symbol.
        /// </summary>
        public static IObservable<T> Debug<T>(this IObservable<T> source, string label = null)
        {
#if DEBUG
            var l = (label == null) ? "" : "[" + label + "]";
            return source.Materialize()
                .Do(x => UnityEngine.Debug.Log(l + x.ToString()))
                .Dematerialize()
                .DoOnCancel(() => UnityEngine.Debug.Log(l + "OnCancel"))
                .DoOnSubscribe(() => UnityEngine.Debug.Log(l + "OnSubscribe"));

#else
            return source;
#endif
        }

        /// <summary>
        /// Debug helper of observbale stream. Works for only DEBUG symbol.
        /// </summary>
        public static IObservable<T> Debug<T>(this IObservable<T> source, UniRx.Diagnostics.Logger logger)
        {
#if DEBUG
            return source.Materialize()
                .Do(x => logger.Debug(x.ToString()))
                .Dematerialize()
                .DoOnCancel(() => logger.Debug("OnCancel"))
                .DoOnSubscribe(() => logger.Debug("OnSubscribe"));

#else
            return source;
#endif
        }
    }
}

よく見てみると、以下のオペレーターが順番に実行されています。

オペレーター 意味
Materialize OnNext・OnError・OnComplete ( を含むメッセージ*2 ) をNotification<T>というオブジェクトに変換してOnNextのメッセージとして出力する
Do OnNextのたびに呼ばれる
Dematerialize Materializeをもとに戻す
DoOnCancel 購読解除された時に呼ばれる
DoOnSubscribe 購読された時に呼ばれる

これを考えると、なぜDebugオペレーターが以下のタイミングでログ出力を行うかが分かります。

  • OnNext
  • OnError
  • OnComplete
  • OnCancel
  • OnSubscribe

*1:これ以外にももしあれば教えていただけると嬉しいです。要調査

*2:少なくともOnNext,OnError,OnCompleteは含みますが、これで全てかどうかは調査できていないです。要調査