16
8

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 1 year has passed since last update.

UnityAdvent Calendar 2023

Day 18

【Unity】UniTaskAsyncEnumerableとは何なのか

Last updated at Posted at 2023-12-17

はじめに

UniTaskのv2が登場し、UniTaskAsyncEnumerableという機能が追加されから早数年が経過しました。
当時、UniTask Ver2 、UniTaskAsyncEnumerableまとめという記事を執筆しました。このときもUniTaskAsyncEnumerableについてまとめているのですが、今回はもうちょっと掘り下げてUniTaskAsyncEnumerableについて解説します。

とくにObservable(UniRx)の代替としてUniTaskAsyncEnumerableを用いてみるパターンが増えてきたと思うので、そのあたりの比較をしていきます。

サンプルコード

この記事中に登場するコードはこちらのGitHubにて公開しています。

UniTaskAsyncEnumerable概要

UniTaskAsyncEnumerableとは「IAsyncEnumerable<T>相当の機能をUnity向けの実装したもの」です。
インタフェースとしてはIUniTaskAsyncEnumerable<T>IAsyncEnumerable<T>に相当しています。

ではIAsyncEnumerable<T>が何なのかというと、読んで字の如く「非同期で扱えるEnumerable」です。

同期的なIEnumerable

ではまずいつもの同期的なIEnumerable<T>がどのような挙動をするのか見てみます。
といってもIEnumerable<T>だけ取り出してもわかりにくいので、「IEnumerable<T>foreachで列挙する」ときの挙動を見てみましょう。

たとえば次のコードを見て下さい。

// 同期的ないつものEnumerable
private static void EnumerableSample(IEnumerable<int> data)
{
    foreach (var i in data)
    {
        Console.WriteLine(i);
    }
}

このコードはシンプルに「IEnumerable<int>foreachで列挙してログに出す」というだけの処理です。
ここでforeachを使っていますが、実はこのコードは実行時にコンパイラによって次のようなコードに書き換えられてします。

private static void EnumerableSample(IEnumerable<int> data)
{
    // foreachは次のコードと同義である
    var enumerator = data.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var i = enumerator.Current;
            Console.WriteLine(i);
        }
    }
    finally
    {
        enumerator.Dispose();
    }
}
  1. IEnumerable<T>からIEnumerator<T>を取得する
  2. IEnumerator<T>MoveNext()を実行して「次の要素」を1つ取り出す
  3. MoveNext()から次の要素が取得できたら、その値を使って「処理」を実行する
  4. MoveNext()から次の要素が取得できない(要素が尽きた)場合は終了

これが同期的なEnumerableforeachの挙動です。
値を順番に同期的に列挙していき、その中身が尽きたら終了という動作をしています。

非同期的なIAsyncEnumerable

では一方で非同期的なIAsyncEnumerable<T>がどうなるのか。
こちらも実際にコードを見たほうが早いです。

// 非同期的なAsyncEnumerableの列挙
private static async ValueTask EnumerableSampleAsync(IAsyncEnumerable<int> data)
{
    await foreach (var i in data)
    {
        Console.WriteLine(i);
    }
}

見た目はほとんど同じですが、await foreachに変化している部分が特徴です。
そしてこのコードはコンパイラによって次のコードに変換されます。

// 非同期的なAsyncEnumerableの列挙
private static async ValueTask EnumerableSampleAsync(IAsyncEnumerable<int> data)
{
    // await foreachは次のコードと同義である
    var enumerator = data.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var i = enumerator.Current;
            Console.WriteLine(i);
        }
    }
    finally
    {
        await enumerator.DisposeAsync();
    }
}

最大の違いは「await enumerator.MoveNextAsync()」と、MoveNext()が非同期になっているところです。
つまり「値の列挙を要求したときに、その要素が返って来るのを1つずつ待てる」というのがIAsyncEnumerable<T>の特徴です。

EnumerableとObservable、そしてAsyncEnumerable

さて、Enumerableのその対となる概念としてObservable(UniRx)があります。
このObservableAsyncEnumerableは何が異なるのでしょうか。

IAsyncEnumerableがまだ存在しなかった時代、「IObservableIEnumerableと双対である」といわれていました。

SyncAsync.jpg

というのも、IEnumerableはPull型駆動であるのに対して、IObservableはPush型駆動(本質的に非同期)でした。
この2つのインタフェースは非常によく似ており、実際にインタフェースをひっくり返して定義すれば双対であることがわかります。
参考: Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方

ということで、「EnumerableのPull/Pushを逆にし、非同期にコレクションを扱えるようにしたものがObservableだよ」という説明を今までは行うことができました。

ですが、AsyncEnumerableの登場により話がややこしくなりました。Enumerableの非同期版がObservableというのであれば、じゃあAsyncEnumerableは何者なのか。さっきの図は嘘なのか。AsyncEnumerableをさきほどの図に追加するとどうなるのか。自分はこうなると考えます。

SyncAsync2.jpg

普通に「非同期・複数」の枠に2つ収まるかなと考えます。

では根本的にAsyncEnumerableObservableが何が違うのかというと、やはりそれは「PushであるかPullであるか」です。

PushとPull

非同期におけるPushとPullの違いを表にすると次のとおりになります。

挙動の違い
Push IObservable<T> 非同期処理の「実行側」に制御権がある。いつ値が発行されるかは待受側にはわからない。
Pull IAsyncEnumerable<T> 非同期処理の「待受側」に制御権がある。待受側が要求しない限り値が発行されることがない。

image.png

Observableでは非同期処理の実行側が制御を握っています。つまり非同期処理がいつどのようなタイミングで実行され、その結果がいつ発行されるのかはすべてObservableの内部が管理しています。そのためSubscribe側では「いつか発行される値を待つ」ということしかできません。

一方のAsyncEnumerableでは非同期処理の待受側が制御を握っています。つまり待受側の準備が整って初めて非同期処理が実行されます。そのため待受側
でメッセージの流量の制御が行えます。

ObservableとUniAsyncEnumerableの使い分け

ObservableUniAsyncEnumerableの使い分けをどう使い分けるか。
Unityにおいては両者を同じ雰囲気で使える場合と、明確に区別したほうがいい場面があります。

というわけでここからは「UniRxのObservable」と「UniTaskのUniTaskAsyncEnumerable」に話を限定していきます。

だいたい似た使い方ができるパターン

  • Unityのイベントメッセージの待ち受け
  • AsyncReactiveProperty

購読が一箇所のみ、つまり「どこか一箇所だけでSubscribeまたはawaitしている」みたいな状況の場合。このときはObservableUniAsyncEnumerableも似た使い方ができます。

Unityのイベントメッセージの待受

Unityでよくあるのは「UIイベントの待機」が挙げられます。「UIボタンが押されたら処理を行う」といったイベント処理はObservableで実装してもUniAsyncEnumerableで実装しても似た形で扱えます。

using System.Threading;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.UIEventDiff
{
    public class UIEventDiffSample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private Text _outputText;

        private void Start()
        {
            SubscribeAsObservable(destroyCancellationToken);
            SubscribeAsAsyncEnumerable(destroyCancellationToken);
        }

        // ButtonをObservableとして購読
        private void SubscribeAsObservable(CancellationToken ct)
        {
            // OnClickAsObservableでObservableに変換できる
            _buttonA.OnClickAsObservable()
                .Subscribe(_ =>
                {
                    _outputText.text += "ButtonA Clicked (Observable)!\n";
                })
                .AddTo(ct);
        }

        // ButtonをUniTaskAsyncEnumerableとして購読
        private void SubscribeAsAsyncEnumerable(CancellationToken ct)
        {
            // OnClickAsAsyncEnumerableでUniTaskAsyncEnumerableに変換できる
            _buttonA.OnClickAsAsyncEnumerable(ct)
                .Subscribe(_ =>
                {
                    _outputText.text += "ButtonA Clicked (UniTaskAsyncEnumerable)!\n";
                }, ct);
        }
    }
}

UIA.gif

このようなUniTaskが提供するUnityイベントをUniTaskAsyncEnumerableに変換してくれる機構はObservable(UniRx)版とだいたい同じ感覚で使えます。

ただしUniRxOperatorと、UniTaskLINQは完全に互換ではないので、まったく同じように使えるわけではありません。Operatorの組み合わせでやっていたことをUniTaskAsyncEnumerableで再現しようとした場合は工夫が必要になります。

AsyncReactiveProperty

UniRxにはReactivePropertyと呼ばれるObservableベースの機構がありましたが、これのUniTaskAsyncEnumerableとしてAsyncReactivePropertyがあります。

こちらについては以前にまとめているのでまずはこちらを参照してください。

基本的な挙動はUniRx.ReactivePropertyとだいたい同じです。ただAsyncReactivePropertyasync/awaitと組み合わせたときに扱いやすくなっているため、その点はUniRx版より優れています。

挙動が異なるので気をつけたほうがよいパターン

一方でObservableUniTaskAsyncEnumerableとで挙動が大きく異なる場合があります。
根本から大きく挙動が変わる場合があるので注意してください。

メッセージを受け取った「後」の非同期処理の実施

Observableの場合

まずそもそも、Observableではメッセージ購読と非同期処理(async/await)を組み合わせることができません。Subscribe()メソッドの中でasync/awaitを実行したとしても、それはObservableの挙動に一切干渉しません(できません)。

たとえば次のような実装があったとします。「ボタンを押したときに通信をするが、連打防止のために1回実行したら数秒awaitを挟んで次のイベントを受け取らないようにする」というもの。しかしこれは意図した動作をしません。

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.ObservableAsync
{
    public class ObservableAsyncSample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private InputField _urlInputField;
        [SerializeField] private Text _outputText;

        private void Start()
        {
            SubscribeAsObservable(destroyCancellationToken);
        }

        // ButtonをObservableとして購読
        private void SubscribeAsObservable(CancellationToken ct)
        {
            // ボタンが押されたら通信を実行
            // 通信結果をTextに表示する
            _buttonA.OnClickAsObservable()
                 // ここでasync/awatiを使おうとしている
                .Subscribe(async _ =>
                {
                    Debug.Log("<color=red>実行開始!</color>");
                    _outputText.text = "";

                    var url = _urlInputField.text;

                    // サーバに問い合わせる
                    var result = await FetchAsync(url, ct);
                    _outputText.text = result;

                    // ボタンを連打したときに一気に通信しないように3秒待たせる
                    await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: ct);

                    Debug.Log("<color=green>実行終了!</color>");
                })
                .AddTo(ct);
        }

        private async UniTask<string> FetchAsync(string url, CancellationToken ct)
        {
            using var request = UnityWebRequest.Get(url);
            await request.SendWebRequest().ToUniTask(cancellationToken: ct);

            return request.downloadHandler.text;
        }
    }
}

ObservableAsyncSample.csではObservableのSubscribe上でasync/awaitの実行を行っています。
意図としては「ボタンが押されたときに通信を行うが、このときに通信が頻発しないように3秒間の待機時間を作る」です。

ですがこれは意図どおりに動きません。ボタンを連打するとその回数だけ処理が実行されます。

UI_Renda.gif

これはなぜかというと、ObservableのSubscribe()async/awaitをケアしてくれないためです。Subscribe()の中でasync/awaitを使ったとしても、それはasync voidで呼び出した扱いになってしまいます。

// ButtonをObservableとして購読
private void SubscribeAsObservable(CancellationToken ct)
{
    // ボタンが押されたら通信を実行
    // 通信結果をTextに表示する
    _buttonA.OnClickAsObservable()
        .Subscribe(_ =>
        {
            // async voidのメソッドを呼び出してるのと何も変わらない
            InnerFunction(ct);
        })
        .AddTo(ct);
}

private async void InnerFunction(CancellationToken ct)
{
    Debug.Log("<color=red>実行開始!</color>");
    _outputText.text = "";

    var url = _urlInputField.text;

    // サーバに問い合わせる
    var result = await FetchAsync(url, ct);
    _outputText.text = result;

    // ボタンを連打したときに一気に通信しないように3秒待たせる
    await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: ct);

    Debug.Log("<color=green>実行終了!</color>");
}

ObservableAsync.jpg

つまりObservableの場合はメッセージのハンドリングに非同期処理を絡めることができません。
Operator使えばメッセージの「流量」を制限することはできるにはできます。が、Operatorasync/awaitには未対応です)

UniTaskAsyncEnumerableの場合

ではUniTaskAsyncEnumerableの場合どうなるかですが、Observableと違ってメッセージ購読にasync/awaitを併用することができます。

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.UniTaskAsyncEnumerableAsync
{
    public class UniTaskAsyncEnumerableAsyncSample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private InputField _urlInputField;
        [SerializeField] private Text _outputText;

        private void Start()
        {
            SubscribeAsUniTaskAsyncEnumerable(destroyCancellationToken);
        }

        // ButtonをObservableとして購読
        private void SubscribeAsUniTaskAsyncEnumerable(CancellationToken ct)
        {
            // ボタンが押されたら通信を実行
            // 通信結果をTextに表示する
            _buttonA.OnClickAsAsyncEnumerable(ct)
                // ForEachAwaitAsyncになっている点に注意
                .ForEachAwaitAsync(async _ =>
                {
                    Debug.Log("<color=red>実行開始!</color>");
                    _outputText.text = "";

                    var url = _urlInputField.text;

                    // サーバに問い合わせる
                    var result = await FetchAsync(url, ct);
                    _outputText.text = result;

                    // ボタンを連打したときに一気に通信しないように3秒待たせる
                    await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: ct);

                    Debug.Log("<color=green>実行終了!</color>");
                }, ct);
        }


        private async UniTask<string> FetchAsync(string url, CancellationToken ct)
        {
            using var request = UnityWebRequest.Get(url);
            await request.SendWebRequest().ToUniTask(cancellationToken: ct);
            return request.downloadHandler.text;
        }
    }
}

UI_Renda2.gif

UniTaskAsyncEnumerableとして購読を行った場合、メッセージのハンドリングにasync/awaitを正しくケアしてくれます。このため意図したとおりに連打防止が働いています。

AsyncEnumerableAsync.jpg

そしてここで、UniTaskAsyncEnumerableのややこしい面がでてきます。
UniTaskAsyncEnumerableではメッセージ購読側でasync/awaitを実行することができました。そのためメッセージのハンドリングタイミングを柔軟に制御できていてObservableの上位互換のようにも見えます。

ですがこの購読側でメッセージのハンドリング中に発行されたメッセージ、つまり「MoveNextAsync()を呼び出す前に発行されたメッセージ」がどのようにハンドリングされるかはIUniTaskAsyncEnumerable<T>の実装側に委ねられています。つまり利用者側にとってメッセージがタイミングによってはキューに積まれるのか、それとも無視されるのか、どっちの挙動になるかがわからないのです。

たとえば先程の「連打防止」ですが、これは「Button.OnClickAsAsyncEnumerableは購読側でawaitしている間に発行されたメッセージはすべて無視される」という前提だからこそ正しく動作しています。
UniTaskYaya.jpg

もし同じような連打防止機構を「キュー機能を持ったUniTaskAsyncEnumerable」に適用した場合、短期間で処理が連発することは防げますでしょうが、連打された回数だけ時間をかけて処理を実行することになってしまいます(こっちの挙動のほうが好ましい、という場合もあるので一概に間違いとは言えない)。

このように、UniTaskAsyncEnumerableはその実装によって挙動が全く変わる場合があるというのを覚えておく必要があります。

複数箇所から同時に購読する場合

そもそも、複数回同時に同じUniTaskAsyncEnumerableを購読すること自体があまり推奨されません。UniTaskAsyncEnumerableに対する購読は一箇所までと制限を課したほうが扱いは簡単になります。

まず同期版のIEnumerableを考えてみます。IEnumerableを2回foreachしたらどうなるでしょうか。正解は2個のイテレータが生成され、それぞれ独立したforeach文が実行されます(それぞれごとに値の列挙が独立して実行される)。

UniTaskAsyncEnumerableもこれと同じであり、購読するたびに新しい非同期イテレータのインスタンスが生成されます。

たとえば次のようなコードがあったとします。IUniTaskAsyncEnumerable<int>を1つ作成し、これを2回購読しています。

using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.UniTaskAsyncEnumerableInstance
{
    public class UniTaskAsyncEnumerableAsyncSample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private Button _buttonB;
        [SerializeField] private Text _outputText;

        // カウントアップを続けるUniTaskAsyncEnumerable
        private IUniTaskAsyncEnumerable<int> CountUpAsyncEnumerable()
        {
            return UniTaskAsyncEnumerable.Create<int>(async (writer, _) =>
            {
                for (var i = 0; i < 10; i++)
                {
                    // 発行したタイムスタンプをログに出す
                    Debug.Log($"{Time.time}: {i}");
                    await writer.YieldAsync(i);
                }
            });
        }

        private void Start()
        {
            // カウントアップを続けるUniTaskAsyncEnumerableをインスタンス化した"つもり"
            var uniTaskAsyncEnumerable = CountUpAsyncEnumerable();

            // ボタンAが押されたら1個取得する
            uniTaskAsyncEnumerable
                .ForEachAwaitAsync(async i =>
                {
                    _outputText.text += $"A: {i}\n";
                    await _buttonA.OnClickAsync(destroyCancellationToken);
                }, destroyCancellationToken);
            
            // ボタンBが押されたら1個取得する
            uniTaskAsyncEnumerable
                .ForEachAwaitAsync(async i =>
                {
                    _outputText.text += $"B: {i}\n";
                    await _buttonB.OnClickAsync(destroyCancellationToken);
                }, destroyCancellationToken);

        }
    }
}

twoButton.gif

実行結果として、2個の独立したUniTaskAsyncEnumerableが生成されて個別に購読されたということがわかります。このようにUniTaskAsyncEnumerableは複数回購読するとそれぞれ独立した非同期ストリームが生成されるというのが基本挙動となります。
(Pull型挙動なので当たり前の話ではある)

では「UniTaskAsyncEnumerableは購読されたら必ず独立した非同期ストリームが生成されるのか」というと、それはNOです(ややこしい)。

たとえばAsyncReactiveProperty<T>ですが、こちら経由でIUniTaskAsyncEnumerable<T>を生成した場合は非同期ストリームのソースはAsyncReactivePropertyのインスタンスそれ1本にまとまります。

using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.AsyncReactivePropertySample
{
    public class AsyncReactivePropertySample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private Text _outputTextA;
        [SerializeField] private Text _outputTextB;

        // カウントアップを続けるUniTaskAsyncEnumerable
        private IUniTaskAsyncEnumerable<int> CreateAsyncEnumerable()
        {
            // AsyncReactivePropertyを使って生成する
            var asyncReactiveProperty = new AsyncReactiveProperty<int>(0);

            UniTask.Void(async () =>
            {
                while (!destroyCancellationToken.IsCancellationRequested)
                {
                    // ボタンが押されたらカウントアップ
                    await _buttonA.OnClickAsync(destroyCancellationToken);
                    asyncReactiveProperty.Value++;
                    
                    Debug.Log($"{Time.time}: {asyncReactiveProperty.Value}");
                }
            });

            Debug.Log("AsyncReactivePropertyを生成しました");
            return asyncReactiveProperty;
        }

        private void Start()
        {
            // カウントアップを続けるUniTaskAsyncEnumerableを作成
            // 実体はAsyncReactiveProperty
            var uniTaskAsyncEnumerable = CreateAsyncEnumerable();

            // 購読してText Aに出力する
            uniTaskAsyncEnumerable
                .ForEachAsync(i =>
                {
                    _outputTextA.text += $"A: {i}\n";
                }, destroyCancellationToken);

            // 購読してText Bに出力する
            uniTaskAsyncEnumerable
                .ForEachAsync(i =>
                {
                    _outputTextB.text += $"B: {i}\n";
                }, destroyCancellationToken);
        }
    }
}

AsyncReactiveProperty.gif

(「AsyncReactivePropertyを生成しました」は1回しかログに出ていない。つまり1個のAsyncReactivePropertyが2回購読されていることである。)


以下、補足としてChannelの話。折りたたんでおきます。

Channel.CreateSingleConsumerUnboundedは1回しか購読できない

手続き的にIUniTaskAsyncEnumerable<T>を生成できるChannelという機能もあるのですが、こちらは購読回数が1回に制限されています。
2回以上、同じIUniTaskAsyncEnumerable<T>を購読しようとすると例外が出るようになっています。

using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine;
using UnityEngine.UI;

namespace TORISOUP.UniTaskAsyncEnumerableSamples.ChannelSample
{
    public class ChannelSample : MonoBehaviour
    {
        [SerializeField] private Button _buttonA;
        [SerializeField] private Text _outputTextA;
        [SerializeField] private Text _outputTextB;

        // カウントアップを続けるUniTaskAsyncEnumerable
        private IUniTaskAsyncEnumerable<int> CreateAsyncEnumerable()
        {
            // Channelを使って生成する
            var channel = Channel.CreateSingleConsumerUnbounded<int>();
            var writer = channel.Writer;
            destroyCancellationToken.Register(() => writer.TryComplete());
            
            var value = 0;
            UniTask.Void(async () =>
            {
                writer.TryWrite(value++);

                while (!destroyCancellationToken.IsCancellationRequested)
                {
                    // ボタンが押されたらカウントアップ
                    await _buttonA.OnClickAsync(destroyCancellationToken);
                    writer.TryWrite(value++);
                    Debug.Log($"{Time.time}: {value}");
                }
            });

            Debug.Log("Channelを生成しました");
            return channel.Reader.ReadAllAsync(destroyCancellationToken);
        }

        private void Start()
        {
            // カウントアップを続けるUniTaskAsyncEnumerableを作成
            // 実体はAsyncReactiveProperty
            var uniTaskAsyncEnumerable = CreateAsyncEnumerable();

            // 購読してText Aに出力する
            uniTaskAsyncEnumerable
                .ForEachAsync(i =>
                {
                    _outputTextA.text += $"A: {i}\n";
                }, destroyCancellationToken);

            
            // 購読してText Bに出力する
            // ここで例外が出る
            uniTaskAsyncEnumerable
                .ForEachAsync(i =>
                {
                    _outputTextB.text += $"B: {i}\n";
                }, destroyCancellationToken);
        }
    }
}

Channel.jpg

複数購読についてまとめると

  • UniTaskAsyncEnumerableは購読されるたびに新しい非同期イテレータを生成するのが基本挙動
  • ただしAsyncReactivePropertyを使うと1本にまとまるので、UniRxのReactivePropertyとだいたい同じ使い方できる

ということになります。これを改めて結論を出すとこうなります。

  • UniTaskAsyncEnumerableObservableと同じ感覚でイベント通知などに用いるべきではない
    • UniTaskAsyncEnumerable自体の実装方法でまったく異なる挙動をするため、意図通りに動くかの保証ができない
  • もしイベント通知用途ならばAsyncReactivePropertyを使って、型もIReadOnlyAsyncReactiveProperty<T>として公開するなどしたほうが安全
    • 少なくとも「AsyncReactivePropertyである」ということがわかっていれば大きな事故は起きないはず

全体のまとめ

  • Enumerableの非同期版がAsyncEnumerable
  • AsyncEnumerableのUniTask実装版がUniTaskAsyncEnumerable
  • 似た概念でObservableがあるがこちらとの明確な違いは「Push」であるか「Pull」であるか
  • Observableとだいたい同じ気持ちで使える場面もあるが、使えない場面もある
  • UniTaskAsyncEnumerableはその実装や連結されたLINQメソッドで大きく挙動を変える可能性がある
  • UniTaskAsyncEnumerableに対する複数回の購読は非推奨である
  • わからないならAsyncReactivePropertyからまず触り始めるのがよい

正直、UniTaskAsyncEnumerable難しいです。かなりのマニア向けの機能な感じがしており、万人が常用するような機能ではないと思います。

(個人的な感想)

UniTaskAsyncEnumerableがあるならば、UniRxはもう要らないのでは?」という意見について

この辺も一度まとめてはいます。

それを踏まえて改めて書きます。

これについては「Yes」でもあり、「No」でもあると思います。

UniRxを「Unityイベントの購読」or「ReactiveProperty」くらいにしか使ってなかったのであれば、そのあたりはUniTaskに代替しても同じ様に動くと思います。なのでそういう場合は「UniRxは不要にできるかもしれない」です。

しかし「UniTaskAsyncEnumerableObservableの完全互換ではない」です。ObservableはPush挙動であるがUniTaskAsyncEnumerableはPull挙動であるという点で大きく挙動が異なります。

とくにこの「UniTaskAsyncEnumerableはPullで動作する」ということの意味をちゃんと把握して、内部がどういう挙動になっているのかを把握しないと事故る可能性が高いです(複数回購読時や、QueuePublishを挟んだときの挙動の理解ができなくなるため)。

そのためイベント駆動で使う場合においては、全体的な挙動のわかりやすさや素直さとしてはObservableの方が優れていると自分は思います。

なので、個人的な落とし所としては次のあたりなんじゃないかなぁと思います。


  • UniTaskは最優先で導入して良い
  • UniRxは無くてもなんとかなるが、UniTaskですべて代替は厳しい部分もある
  • Unityイベントの扱いについてはUniTask/Observable/UniTaskAsyncEnumerableどれでも好きなやり方でよい
  • 自分でイベント通知を実装する場合は、Observable/ReactiveProperty/AsyncReactivePropertyを使う
    • 剥き身のIUniTaskAsyncEnumerableをイベント通知用途に使うのは怖すぎるから避ける

そもそもObservableasync/awaitって併用できるし、何ならObservableUniTaskAsyncEnumerableは相互変換が可能です。
なので「UniRxとUniTaskを両方導入していいとこ取りする」ができればそれがベストかなと思います。

あと身も蓋もない話をすると、自分はあまりUniTaskAsyncEnumerableを使ってません。挙動がややこしすぎる上、ピッタリこれがハマって役に立つ場面が限定的だからです。(UnityのUIイベント周りのハンドリングに使うこともあるが、UniRxでも実現できるので自分は併用してます)

16
8
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
16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?