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

# UniTask.ToObservable()

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

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

# 本来なら

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

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


# 試行錯誤

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

```cs
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.Void`と`Observable.Create`でがんばってみる。

```cs
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. 若干汎用化する

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

```cs
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;
        });
}
```

```cs
FromUniTask(token => GetTextAsync("https://unity.com/", token))
    .Subscribe(Debug.Log);
```

これでも動く。

## 3. staticメソッド化する

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

```cs
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;
                });
        }
    }
}
```

```cs
MyProjectAsset.Utils.ObservableConverter
    .FromUniTask(token => GetTextAsync("https://unity.com/", token))
    .Subscribe(Debug.Log);
```

多少はマシになった。


## 4. UniRxに直接書き足す

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

 * [【UniRx.Async】UniTaskのキャンセル覚書【Unity】](https://qiita.com/su10/items/ccb12742ad0be790b323#%E3%82%AD%E3%83%A3%E3%83%B3%E3%82%BB%E3%83%AB%E3%82%92%E8%80%83%E6%85%AE%E3%81%97%E3%81%9Funitaskiobservable%E5%A4%89%E6%8F%9B)

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

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

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

![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/775c1e8e-6839-a6d1-3094-b8a0bcc57923.jpeg)


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

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

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

![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/384ee2c9-64a7-03a8-6b68-35cf8479c426.jpeg)


### Ⅲ. Observable.FromUniTaskを定義する

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

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

```cs
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.動かす

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

これで動作した。

# 感想

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

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

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