【Unity】【UniRx】UniRxで例外を取り扱う方法まとめ

UniRxで例外を取り扱う方法をまとめました。

Unity2018.4.0

UniRxによる例外処理の必要性

まず、下記のように例外を発生させるだけのストリームを作ってみます。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .Subscribe()
            .AddTo(this);
    }
}

これを再生してみます。
Unityで例外が発生するとConsoleにエラーが出ますが、これを再生してもエラーが出ないことがわかります。

f:id:halya_11:20190907122756p:plain

これはUniRxどうこうではなく、単純にメインスレッド以外で例外が発生しているからです。
試しに上記のコードを下記のようにメインスレッド内で例外が発生するように書き換えてみます。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .ObserveOnMainThread() // メインスレッドで処理をする
            .Subscribe()
            .AddTo(this);
    }
}

この状態で再生をするとConsoleにエラーが表示されることがわかります。

f:id:halya_11:20190907122941p:plain

このように、UniRxは簡単にマルチスレッドの処理を書ける分、
エラーハンドリングはしっかりと行う必要があります(UniRxに限らずしっかりやらないとですが)。

そこで、この記事ではUniRxで例外を取り扱う方法をまとめます。

Subscribeの引数のonErrorで処理する

一番基本的な方法はSubscribeの引数のonErrorに例外発生時の処理を書くことです。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .Subscribe
            (
                _ => { },
                ex => Debug.Log("例外発生 : " + ex) // onErrorに例外発生時の処理を書く
            )
            .AddTo(this);
    }
}

onErrorはストリーム内で例外が発生した時に例外のインスタンスと共に呼ばれます。

Catchオペレータで例外を補足する

Subscribe()よりも前に例外処理を書きたい場合にはCatch()を使います。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .Catch((Exception ex) => {
                // 例外が発生した時の処理を記述
                return Observable.Start(() => Debug.Log("例外発生 : " + ex));
            })
            .Subscribe()
            .AddTo(this);
    }
}

ここで、Catch()オペレータの戻り値はIObservableになっています。
つまり例外を補足したらその例外を処理して、代わりのObservableを渡すことができます。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .ObserveOnMainThread()
            .Catch((Exception ex) => {
                // 60フレーム後にログ出力して後続処理を流す
                return Observable.TimerFrame(60)
                    .Do(x => Debug.Log("例外発生 : " + ex))
                    .AsUnitObservable();
            })
            .Subscribe(_ => { }, () => Debug.Log("On Completed"))
            .AddTo(this);
    }
}

これにより例えば例外補足時にダイアログを出して、
そのあとの処理を行うObservableを後続に渡すといったことができます。

またCatch()オペレータには指定した型の例外のみを補足するという便利な機能があります。

using System;
using UniRx;
using UnityEngine;

public class Exception01 : Exception { }
public class Exception02 : Exception { }

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable
            .Start(() => {
                // Exception01かException02のどちらかを発生させる
                var rand = new System.Random();
                if (rand.Next() % 2 == 0) throw new Exception01();
                else throw new Exception02();
            })
            .Catch((Exception01 ex) => Observable.Start(() => Debug.Log("Exception01"))) // Exception01を補足
            .Catch((Exception02 ex) => Observable.Start(() => Debug.Log("Exception02"))) // Exception02を補足
            .Catch((Exception ex) => Observable.Start(() => Debug.Log("Exception"))) // それ以外の例外はここで補足
            .Subscribe()
            .AddTo(this);
    }
}

Catch時に後続処理が必要ない場合にはCatchIgnoreを使う

前節のように、CatchオペレータではIObservableを返すことで後続処理を行うストリームを定義できました。
このような後続処理が必要ない場合にはCatchオペレータの戻り値にEmptyなストリームを渡します。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable
            .Start(() => throw new Exception())
            .Catch((Exception ex) => {
                Debug.Log("Exception01");
                // すぐにこのストリームが完了(Complete)する
                return Observable.ReturnUnit();
            })
            .Subscribe(_ => { }, () => Debug.Log("Complete"))
            .AddTo(this);
    }
}

ただこれは若干冗長です。
CatchIgnoreオペレータを使うと上記と同等の処理が以下のように書けます。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable
            .Start(() => throw new Exception())
            .CatchIgnore((Exception ex) => Debug.Log("Exception01"))
            .Subscribe(_ => { }, () => Debug.Log("Complete"))
            .AddTo(this);
    }
}

エラーが起きた時に購読しなおすRetryオペレータ

Retry()オペレータを使うと、エラーが起こった時にストリームを再購読してくれます。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable
            .Start(() => throw new Exception())
            .DoOnError(ex => Debug.Log(ex))
            .Retry(3)
            .Subscribe(_ => { }, () => Debug.Log("Complete"))
            .AddTo(this);
    }
}

引数にはリトライ数を渡せます。

特定の例外のみRetryするにはOnErrorRetryを使う

前節のRetry()はどんな例外が発生しても動作してしまいます。
「この例外が発生したときのみリトライする」という処理を実装したい場合にはOnErrorRetry()を使います。

using System;
using UniRx;
using UnityEngine;

public class Exception01 : Exception { }
public class Exception02 : Exception { }

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable
            .Start(() => {
                // Exception01かException02のどちらかを発生させる
                var rand = new System.Random();
                if (rand.Next() % 2 == 0) throw new Exception01();
                else throw new Exception02();
            })
            // Exception01だったら繰り返す
            .OnErrorRetry((Exception01 ex) => Debug.Log("Exception01"))
            // Exception01だったらComplete
            .CatchIgnore((Exception02 ex) => Debug.Log("Exception02"))
            .Subscribe()
            .AddTo(this);
    }
}

例外発生時に処理はするけど補足はしないDoOnError

これまで紹介したオペレータは例外を補足するため、補足後にはOnErrorが発生しなくなります。
例外発生時に何かしらの処理を行いたいけど補足する必要がない場合にはDoOnError()オペレータを使います。

using System;
using UniRx;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        Observable.Start(() => throw new Exception())
            .ObserveOnMainThread()
            .DoOnError(ex => Debug.Log("例外発生 : " + ex))
            .Subscribe()
            .AddTo(this);
    }
}

余談ですが、関連するオペレータにDoOnTerminate()Finish()があります。
これらもOnError時に処理を行えるオペレータになります。
詳細は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

関連

light11.hatenadiary.com