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

UniRxで`Rx`と`async/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の完全でハイパフォーマンスな統合](http://neue.cc/2018/07/12_567.html) より引用


## ケース1

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

### 実装
#### Rx

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

#### async/await

```csharp
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

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

#### async/await

```csharp
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

```csharp
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

```csharp
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

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

#### async/await

```csharp
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

```csharp
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

```csharp
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

```csharp
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

```csharp
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

```csharp
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

```csharp
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`の難しさだと思いますが。)