滅入るんるん

何か書きます

【C#】IObservable<T>の最初のイベントだけをawaitで待機する話

唐突ですが、最近やっと仕事でもXamarin.Androidを使い始めました。

仕事で書いてるプロジェクトではMVVM的な感じでViewModel側にReactivePropertyを使っていて、View側を担当しているのでViewModel側が用意したReactivePropertyを購読してやればいいのですが、Viewの初期化時に一度だけイベントをフックしたい場面に遭遇しました。

本来そういう場面には遭遇しないはずではと思いつつしょうがないので一度イベント(OnNext)が飛んで来たら購読解除するような処理を書くことに。まぁどうせならasync/awaitのawaitで待機したいよね、というか待機できたらスマートだよね、という感じで沼にハマっていくことに。

ぱっと調べた感じでは終了イベント(OnComplete)を待機する方法は標準であるようですが、初回イベントだけを待機するものはなさそうでした。
※ほんとはあるかもしれないけど自分は見つけられなかったので、そういう方法が存在するなら教えてください🙏

なければ作ればいいじゃん

blog.meilcli.net

過去にTask-LikeとかAwaitableパターンとかかじってる私ならそれぐらい作れるだろう(脳筋)ということで、すぐに作っちゃいました。(まぁコード的には大したことないのでほんとにすぐだったんですが)
ただ仕事のほうではあまりにも怖すぎて採用見送りましたけどね

github.com

なんだかんだあって、落ち着いたときにそれなりのテストコードも用意しようという感じで用意してGithubに上げておきました。採用は自己責任で、というかnugetにすらupしてないですが

仕組みの解説

Awaitableパターンですが、AwaiterとAwaitableをコンパイラーが望む形で用意してあげればawaitできるようになります。
望む形というのがポイントで、ダックタイピング的な感じで用意してあげる必要があるのです。

    public class Awaiter<T> : ICriticalNotifyCompletion
    {
        public bool IsCompleted { get; private set; }

        public void OnCompleted(Action continuation)

        public void UnsafeOnCompleted(Action continuation)

        public T GetResult()
    }

いろいろと省いてますが、Awaiterの最小構成はこんな感じです。ICriticalNotifyCompletionじゃなくてINotifyCompletionでもいい気配はしますが、ValueTaskで使われているValueTaskAwaiterがそんな感じなので、そうしてます(深い理由は知りません)。

    public class Awaitable<T>
    {
        public Awaiter<T> GetAwaiter()
    }

Awaitableもいろいろ省いてますが、こちらは単純にAwaiterを返すだけのGetAwaiterメソッドがあればいいだけです。

Awaitableパターンの説明はこのあたりまでにして、実際にawaitするときの挙動のことにも触れておかないといけませんね。

f:id:meilcli:20181118144500p:plain

画像は用意したテストコードをILSpyでデコンパイルしたものです。

デコンパイル結果がかなり意訳付きのコードになってますがそれはしょうがないとして、if(!waitFirstAwaiter.IsCompleted)でawaitするかの分岐が用意されてます。これは単純にawait対象がすでに処理を完了させてたら処理を待機しなくていいだろというショートサーキット的な感じのものですが、Awaitableパターンとして実装するからにはこの分岐に対応させなければいけません。
また意訳されすぎて表舞台に上がってきてませんが、ここではAsyncTaskMethodBuilderが存在しています。もしwaitFirstAwaiter.IsCompletedがfalseなら、AsyncTaskMethoBuilderが状態機械的な動作をします。いろいろ省いて説明すると、Awaiter.OnCompletedAwaiter.UnsafeOnCompletedの引数のActionがawait後の処理として降ってくるので、処理完了後に呼び出してあげる必要があります。

という感じで実装したのがWaitFirstAwaiterになります。

ObservableWaitFirst/WaitFirstAwaiter.cs at master · MeilCli/ObservableWaitFirst · GitHub

さいごに

自分はまだまだこのあたりの知識が浅いのでいろいろと間違ってるかもしれませんが、間違っていたらコメントで教えてくれると助かりますm(__)m

あと、OnErrorどうするのっていう話はありますが、どのように例外を処理させたいかで変わると思いますのでお好みのチューニングということで🙏

まぁ、おとなしくawaitしないで書いたほうが安全かとは思いますが、時代はasync/awaitなので。