15
7

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 5 years have passed since last update.

UniTaskをCancellationTokenを指定しながらToObservableするメモ

Last updated at Posted at 2019-07-22

UniTask.ToObservable()

UniTaskからObservableに変換したい場合、UniTask.ToObservable()を使う必要がある。
しかしこの方法だとCancellationTokenを指定できない。

そこでCancellationTokenを用いてToObservableする方法を模索してみた。
そのメモです。

本来なら

Observable.FromAsync()がUniRxに用意されているはずだが、それがない。
(そもそもあったとしてもTask用であって、UniTask用ではないと想像できる)。

そこで、Observable.FromAsync()相当のものを自分で組み立てることにする。

試行錯誤

次のようなUniTask<string>があったとして、これをObservableに変換してみる。

private async UniTask<string> GetTextAsync(string uri, CancellationToken token)
{
    var uwr = UnityWebRequest.Get(uri);

    // SendWebRequestが終わるまでawait 
    await uwr.SendWebRequest()
        .ConfigureAwait(cancellation: token);

    if (uwr.isHttpError || uwr.isNetworkError)
    {
        // 失敗していたらそのまま例外をthrow
        throw new Exception(uwr.error);
    }

    return uwr.downloadHandler.text;
}

1.あるものでがんばる

UniTask.VoidObservable.Createでがんばってみる。

private IObservable<string> GetTextureObservable(string uri)
{
    return
        Observable.Create<string>(observer =>
        {
            var cd = new CancellationDisposable();

            UniTask.Void(async () =>
            {
                try
                {
                    observer.OnNext(await GetTextAsync(uri, cd.Token));
                    observer.OnCompleted();
                }
                catch (Exception e)
                {
                    observer.OnError(e);
                }
            });
            return cd;
        });
}

インデントがひどいけど、一応動く。
ただラムダ式が外の変数をキャプチャしてしまっているので、Observable.CreateWithStateとか使ったほうがよさそうではある。

2. 若干汎用化する

このままだと使いにくいので、デリゲートを引数として取るようにする。

private IObservable<T> FromUniTask<T>(Func<CancellationToken, UniTask<T>> func)
{
    return
        Observable.Create<T>(observer =>
        {
            var cd = new CancellationDisposable();

            UniTask.Void(async () =>
            {
                try
                {
                    observer.OnNext(await func(cd.Token));
                    observer.OnCompleted();
                }
                catch (Exception e)
                {
                    observer.OnError(e);
                }
            });
            return cd;
        });
}
FromUniTask(token => GetTextAsync("https://unity.com/", token))
    .Subscribe(Debug.Log);

これでも動く。

3. staticメソッド化する

クラスメソッドのままでは使いにくいので、staticメソッド化する。

using System;
using System.Threading;
using UniRx.Async;
using UniRx;

/// <summary>
/// よしなな名前空間で
/// </summary>
namespace MyProjectAsset.Utils
{
    /// <summary>
    /// 適当なクラス名で
    /// </summary>
    public static class ObservableConverter
    {
        public static IObservable<T> FromUniTask<T>(Func<CancellationToken, UniTask<T>> func)
        {
            return
                Observable.Create<T>(observer =>
                {
                    var cd = new CancellationDisposable();

                    UniTask.Void(async () =>
                    {
                        try
                        {
                            observer.OnNext(await func(cd.Token));
                            observer.OnCompleted();
                        }
                        catch (Exception e)
                        {
                            observer.OnError(e);
                        }
                    });
                    return cd;
                });
        }

        public static IObservable<Unit> FromUniTask(Func<CancellationToken, UniTask> func)
        {
            return
                Observable.Create<Unit>(observer =>
                {
                    var cd = new CancellationDisposable();

                    UniTask.Void(async () =>
                    {
                        try
                        {
                            await func(cd.Token);
                            observer.OnNext(Unit.Default);
                            observer.OnCompleted();
                        }
                        catch (Exception e)
                        {
                            observer.OnError(e);
                        }
                    });
                    return cd;
                });
        }
    }
}
MyProjectAsset.Utils.ObservableConverter
    .FromUniTask(token => GetTextAsync("https://unity.com/", token))
    .Subscribe(Debug.Log);

多少はマシになった。

4. UniRxに直接書き足す

このままでも動くけど、パフォーマンス的にはムダが多い予感がする。
なのでUniRxObservable.FromAsyncのUniTask版を直接追加してしまうことにする。

こちらを参考に、UniRx名前空間に直接処理を継ぎ足す。
Observable.StartAsyncObservable.FromAsyncをプロジェクトに追加する。

Ⅰ.パッケージ間の依存を解決する

UniRxUniTaskはそれぞれdllが分割されているため、そのままでは依存関係がない。
そこでUniRxAssembly Definition Filesに、UniRx.Asyncへの依存を追加する。

1.jpg

UniRxのスクリプトが配置されているディレクトリにあるUniRx.asmdefを編集し、UniRx.Asyncへの依存を追加する。

Ⅱ. csファイルを配置する

続いて、UniRxのスクリプト群と同じ場所に新しいcsファイルを追加する。
名前はObservable.FromAsync.csにしたかった**が、**すでに同名のファイルが別の場所にあり混同しそうなため今回はObservable.FromUniTask.csにした。

2.jpg

Ⅲ. Observable.FromUniTaskを定義する

@su10さんの記事を参考に、各メソッドを追加する。
だがなぜかUniRx.IObservable<T>System.IObservable<T>が衝突してエラーになってしまう。
CancellationTokenを引数にとらない場合のみこのエラーとなるため、該当のコードは使わないので削除した。

あとは諸々の型を調整して、次のようになった。

using System;
using System.Threading;
using UniRx.Async;

namespace UniRx
{
    partial class Observable
    {
        public static IObservable<TResult> StartAsync<TResult>(Func<CancellationToken, UniTask<TResult>> functionAsync)
        {
            var cancellable = new CancellationDisposable();

            var task = default(UniTask<TResult>);
            try
            {
                task = functionAsync(cancellable.Token);
            }
            catch (Exception exception)
            {
                return Throw<TResult>(exception);
            }

            var result = task.ToObservable();

            return Create<TResult>(observer =>
            {
                var subscription = result.Subscribe(observer);
                return new CompositeDisposable(cancellable, subscription);
            });
        }

        public static IObservable<Unit> StartAsync(Func<CancellationToken, UniTask> actionAsync)
        {
            var cancellable = new CancellationDisposable();

            var task = default(UniTask);
            try
            {
                task = actionAsync(cancellable.Token);
            }
            catch (Exception exception)
            {
                return Throw<Unit>(exception);
            }

            var result = task.ToObservable().AsUnitObservable();

            return Create<Unit>(observer =>
            {
                var subscription = result.Subscribe(observer);
                return new CompositeDisposable(cancellable, subscription);
            });
        }

        public static IObservable<TResult> FromUniTask<TResult>(Func<CancellationToken, UniTask<TResult>> functionAsync)
        {
            return Defer(() => StartAsync(functionAsync));
        }

        public static IObservable<Unit> FromUniTask(Func<CancellationToken, UniTask> actionAsync)
        {
            return Defer(() => StartAsync(actionAsync));
        }
    }
}

IV.動かす

Observable.FromUniTask(token =>
{
    return GetTextAsync("https://unity.com/", token);
}).Subscribe(Debug.Log);

これで動作した。

感想

あるものを組み合わせれば変換はできるが、ムダが大きい気がする。

とはいえ、UniRxに直接メソッドを追加しようとすると、UniRxUniTaskでパッケージが分離した影響もあり、結構手間がかかる。
(特にadfdll分割されているため、そこの依存を手で解決する手間がある)。

パフォーマンス的に目をつぶるなら、「staticメソッドを自分のプロジェクトに継ぎ足す」が楽だとは思う。

15
7
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
15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?