はなちるのマイノート

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

【C#】UniTaskのIObservable.ToUniTaskをR3でもやりたいときの対処法(FirstAsync・LastAsync)

はじめに

UniRxを利用していたときはIObservable.ToUniTaskを活用したコードをよく書いてました。

// OnNextがされるまで待つ
var result = await subject.ToUniTask(useFirstValue: true, cancellationToken: source.Token);

// OnCompletedがされるまで待つ
var result2 = await subject.ToUniTask(useFirstValue: false, cancellationToken: source.Token);

【UniRx,UniTask】IObservable.ToUniTaskメソッドをうまく使いこなそう - はなちるのマイノート

ただR3の場合はIObservableを実装していないのでこれを活用することはできません。
github.com

どうやって実現しようかなと調べていたところやり方を見つけたので書き残しておこうと思います。

概要

以下のIssueで言及されていて、FirstAsyncLastAsyncを使うと実現できるようです。

In R3, FirstAsync or LastAsync allows you to decide which way to wait.
Usually in conver to Task, the code expects Last, but First is often more useful.

It is natural to want to convert R3 to an awaitable type (that is why FirstAsync and LastAsync exist).
It is true that there is an overhead due to not being able to convert directly, but we do not think it is large enough to be a problem in situations where this is required.

// DeepL翻訳
R3では、FirstAsyncとLastAsyncのどちらで待つかを決めることができる。
通常、Taskに変換する場合、コードではLastを期待するが、Firstの方が便利なことが多い。

R3を待ち型に変換したいと思うのは自然なことだ(だからFirstAsyncとLastAsyncが存在する)。
直接変換できないことによるオーバーヘッドがあるのは事実だが、変換が必要な状況で問題になるほど大きくないと考える。

github.com

やり方

// FirstAsyncを活用するとOnNextまで待つ (ToUniTaskのuseFirstValue=true)
var result1 = await subject.FirstAsync(destroyCancellationToken);

// LastAsyncを活用するとOnComplatedまで待つ (ToUniTaskのuseFirstValue=false)
var result2 = await subject.LastAsync(destroyCancellationToken);

 
重要な内部実装は以下でして、FirstAsync(FirstLastSingleOperation.First)のときはOnNextで値を返し、LastAsnyc(FirstLastSingleOperation.Last)のときは最後のOnNextの値をOnComplatedで返してますね。
github.com

実験コード

private async UniTask Start()
{
    var subject = new Subject<int>();
    subject.Subscribe(x => Debug.Log(x))
        .AddTo(this);
        
    var task = UniTask.Create(async () =>
    {
        // 最初のOnNextを待つ
        var result1 = await subject.FirstAsync(destroyCancellationToken);
        Debug.Log($"{result1} in task");

        // OnCompletedまで待つ(最後にOnNextされた値を取得)
        var result2 = await subject.LastAsync(destroyCancellationToken);
        Debug.Log($"{result2} in task");
    });
        
    subject.OnNext(1);
    await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: destroyCancellationToken);
    subject.OnNext(2);
    await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: destroyCancellationToken);
    subject.OnNext(3);
    await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: destroyCancellationToken);
        
    subject.OnCompleted();
        
    await task;
    subject.Dispose();
}