LoginSignup
45
43

More than 5 years have passed since last update.

[Unity]Rxとasync/awaitの使い分けについて

Posted at

UniRxでRxasync/awaitのどちらを使用すべきか迷うときってあると思います。
今回はケース別に同じ処理を書いてみてどちらのほうが書きやすいかについて検証したいと思います。

ちなみに、どちらを使うべきかという結論はでていて非同期ならasync/await、イベントならRxです。
これだけ聞いてピンとくる方なら特にこの記事を読む必要はないです。

Rx vs Coroutine vs async/await
もう結論が出ていて、async/await一本でOK、です。まずRxには複数の側面があって、代表的にはイベントと非同期。そのうち非同期はasync/awaitのほうがハンドリングが用意です。そしてコルーチンによるフレームベースの処理に関してはUniTask.DelayやYieldが解決しました。ので、コルーチン→出番減る, async/await → 非同期, Rx → イベント処理 というように分離されていくと思われます。

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合 より引用

ケース1

ボタンを押したときに何か処理をする。
ボタンは何回でも押せる。

実装

Rx

void Start()
{
    this.button.OnClickAsObservable()
        .Subscribe(_ =>
        {
            Debug.Log("Click!!!");
        })
        .AddTo(this);
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        var handler = this.button.GetAsyncClickEventHandler(token);
        while (true)
        {
            await handler.OnClickAsync();
            Debug.Log("Click!");
        }
    });
}

評価

これはイベント処理なのでRxが書きやすいです。
async/awaitで書くと処理が長くなります。

ケース2

ボタンを押したときに何か処理をする。
ボタンを押せるのは1回のみ。
シーン遷移などを想定したケース。

実装

Rx

void Start()
{
    this.button.OnClickAsObservable()
        .FirstOrDefault()
        .Subscribe(_ =>
        {
            Debug.Log("Click!!!");
        })
        .AddTo(this);
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        await this.button.OnClickAsync(token);
        Debug.Log("Click!");
    });
}

評価

これはイベント処理ですが個人的にはどちらでもいいレベルという感じです。
ケース1のasync/awaitではGetAsyncClickEventHandlerを使用していましたが、
一度しか待たないのでOnClickAsyncを直接使用しています。

ケース3

ボタンを押したときにWebサーバから情報を取得する。
ボタンは前回の実行結果を待たずに何回でも押せる。
ケース1の亜種で何か処理をするという部分が非同期処理になっています。

実装

Rx

void Start()
{
    this.button.OnClickAsObservable()
        .SelectMany(_ => Fetch("https://www.google.co.jp"))
        .Subscribe(body => Debug.Log(body))
        .AddTo(this);
}


// Observableなgetリクエスト
IObservable<string> Fetch(string uri)
{
    // ...
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        var handler = this.button.GetAsyncClickEventHandler(token);
        while (true)
        {
            await handler.OnClickAsync();
            UniTask.Void(async () =>
            {
                var body = await Fetch("https://www.google.co.jp", token);
                Debug.Log(body);
            });
        }
    });
}


// awaitableなgetリクエスト
async UniTask<string> Fetch(string uri, CancellationToken token)
{
    // ...
}

評価

イベント処理と非同期合わせたケースなのですが今回のケースではRxのほうがすっきりしているように見えます。
async/awaitではUniTask.Voidがネストしていて複雑に感じます。

ケース4

ボタンを押したときにWebサーバから情報を取得する。
ボタンは前回の実行結果が出た後にもう一度押せるようになる。
情報の取得中はモーダルビューを表示してボタンのクリックができない状態にする。

実装

Rx

void Start()
{
    this.button.OnClickAsObservable()
        .SelectMany(_ =>
        {
            ShowModal();
            return Fetch("https://www.google.co.jp");
        })
        .Subscribe(body =>
        {
            Debug.Log(body);
            HideModal();
        })
        .AddTo(this);
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        var handler = this.button.GetAsyncClickEventHandler(token);
        while (true)
        {
            await handler.OnClickAsync();
            ShowModal();
            var body = await Fetch("https://www.google.co.jp", token);
            Debug.Log(body);
            HideModal();
        }
    });
}

評価

ケース3とほぼ同じですがasync/awaitのほうは少しシンプルになっています。
どっちで書いても大差ない感じです。
厳密にいえばRx版とasync/await版で処理が違う(仮にモーダルが出てなかった場合、Rx版は連打すると何度も実行されてしまう)のですが許容範囲でしょう。

ケース5

ボタンを押したときにWebサーバから情報を取得する。
ボタンを押せるのは1回のみ。
情報の取得中はモーダルビューを表示してボタンのクリックができない状態にする。

実装

Rx

void Start()
{
    this.button.OnClickAsObservable()
        .FirstOrDefault()
        .SelectMany(_ =>
        {
            ShowModal();
            return Fetch("https://www.google.co.jp");
        })
        .Subscribe(body =>
        {
            Debug.Log(body);
            HideModal();
        })
        .AddTo(this);
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        await this.button.OnClickAsync(token);
        ShowModal();
        var body = await Fetch("https://www.google.co.jp", token);
        Debug.Log(body);
        HideModal();
    });
}

評価

Rxはケース5とほぼ同じですがasync/awaitはまたシンプルになりました。
これならasync/awaitで書くかなという感じです。

ケース6

ボタンが2つあり、1つ目のボタンを押すと1秒ごとに処理を行う処理を開始する。
1つ目のボタンを複数回押すと複数処理が走る。
2つ目のボタンを押すと1秒ごとに行う処理をすべて停止する。

実装

Rx

void Start()
{
    var id = 0;
    this.button.OnClickAsObservable()
        .SelectMany(_ =>
        {
            var _id = id++;
            return Observable.Interval(TimeSpan.FromSeconds(1f))
                .TakeUntil(this.button2.OnClickAsObservable())
                .Select(__ => _id);
        })
        .Subscribe(_id =>
        {
            Debug.Log($"{_id} task.");
        })
        .AddTo(this);
}

async/await

void Start()
{
    var id = 0;
    var token = this.GetCancellationTokenOnDestroy();
    var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
    UniTask.Void(async () =>
    {
        while (true)
        {
            await this.button.OnClickAsync(token);
            var _id = id++;
            UniTask.Void(async () =>
            {
                while (true)
                {
                    await UniTask.Delay(1000, cancellationToken: linkedTokenSource.Token);
                    Debug.Log($"{_id} task.");
                }
            });
        }
    });

    UniTask.Void(async () =>
    {
        while (true)
        {
            await this.button2.OnClickAsync(token);
            linkedTokenSource.Cancel();
            linkedTokenSource.Dispose();
            linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
        }
    });
}

評価

実行中の処理を途中で止めたり加工したりするケースではRxのほうがシンプルに書けます。

ケース7

ボタンが3つあり、すべてのボタンを1度以上押すと処理をする。
処理が走った後は最初の状態に戻る。

実装

Rx

void Start()
{
    var version = 0;
    IObservable<int> Wrap(Button b) => b.OnClickAsObservable().Select(_ => version);

    Observable.CombineLatest(Wrap(this.button), Wrap(this.button2), Wrap(this.button3))
        .Where(xs => xs.All(x => x == version))
        .Subscribe(_ =>
        {
            Debug.Log("Click!");
            version++;
        });
}

async/await

void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Void(async () =>
    {
        var handler = this.button.GetAsyncClickEventHandler(token);
        var handler2 = this.button2.GetAsyncClickEventHandler(token);
        var handler3 = this.button3.GetAsyncClickEventHandler(token);

        while (true)
        {
            await UniTask.WhenAll(handler.OnClickAsync(), handler2.OnClickAsync(), handler3.OnClickAsync());
            Debug.Log("Click!");
        }
    });
}

評価

複数のタスクの完了を待ってから何か処理をするというケースではasync/awaitのほうがシンプルに書けます。

まとめ

最初にも書きましたが基本的には非同期ならasync/await、イベントならRxです。
完全にどちらかだけを使うのではなく状況に応じて最適な方法を選びましょう。

個人的な感想ですがRxは学習コストが高いことや最適な方法が分かりづらいこともあって難しいです。
それに比べてasync/awaitは従来の逐次処理とほとんど変わらないように見えるのでパッと見たときにわかりやすいです。
どちらで書いても同じような複雑さの場合はasync/awaitで書き、Rxのほうがシンプルかつ分かりやすい方法で書けるならそちらを使うと思います。
今回上げたケースの中でRx版はもっとこう書けるというのがあれば編集リクエストかコメントで教えてください。
(結論ありきで書いたのでもっと効率的な方法もあると思います。もっともそういうところがRxの難しさだと思いますが。)

45
43
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
45
43