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の完全でハイパフォーマンスな統合 より引用
ケース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
の難しさだと思いますが。)