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.Void
とObservable.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に直接書き足す
このままでも動くけど、パフォーマンス的にはムダが多い予感がする。
なのでUniRx
にObservable.FromAsync
のUniTask版を直接追加してしまうことにする。
こちらを参考に、UniRx
名前空間に直接処理を継ぎ足す。
Observable.StartAsync
とObservable.FromAsync
をプロジェクトに追加する。
Ⅰ.パッケージ間の依存を解決する
UniRx
とUniTask
はそれぞれdllが分割されているため、そのままでは依存関係がない。
そこでUniRx
のAssembly Definition Files
に、UniRx.Async
への依存を追加する。
UniRxのスクリプトが配置されているディレクトリにあるUniRx.asmdef
を編集し、UniRx.Async
への依存を追加する。
Ⅱ. csファイルを配置する
続いて、UniRxのスクリプト群と同じ場所に新しいcsファイルを追加する。
名前はObservable.FromAsync.cs
にしたかった**が、**すでに同名のファイルが別の場所にあり混同しそうなため今回はObservable.FromUniTask.cs
にした。
Ⅲ. 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
に直接メソッドを追加しようとすると、UniRx
とUniTask
でパッケージが分離した影響もあり、結構手間がかかる。
(特にadf
でdll
分割されているため、そこの依存を手で解決する手間がある)。
パフォーマンス的に目をつぶるなら、「staticメソッドを自分のプロジェクトに継ぎ足す」が楽だとは思う。