非同期 EventHandler について議論するとき、私たちの多くが最初に頭に浮かぶのは、それが唯一の例外であるということです。
以前にこれについて書いたとき、(残りの髪を引き抜きたくない) 非同期ボイドの存在を実際に許可することを含む解決策を模索していたことに興奮しました。
私にとって、これは、問題を完全に回避するためのソリューションを提供するというよりも、非同期の EventHandler を克服するために使用できるいくつかの巧妙なトリックに関するものでした。
とはいえ、この記事には多くの注目が集まっており、非常に感謝しています。一部の人々は、非同期 EventHandler を別の方法で解決したいという意見を表明しました。
これは素晴らしい点だと思ったので、async void を修正しない代わりのアプローチを思いつきたいと思いましたが、いくつかの課題を解決しながら、a-void を完全に回避できます (私がそこで何をしたかを参照してください)。非同期の EventHandler を使用します。
この記事では、独自のコードで試すことができる別のソリューションを紹介します。どのように使用できるかに関して、私の観点から長所と短所を取り上げます。これにより、ユース ケースに適しているかどうかを判断できます。
.NET fiddle right で対話可能なコードを見つけることもできます
非同期 EventHandler で直面する問題は、デフォルトで C# でサブスクライブできるイベントのシグネチャが次のようになることです。
void TheObject_TheEvent(object sender, EventArgs e);
そして、この署名の前を void にすることで、イベントをサブスクライブするために、独自のハンドラーで void を使用する必要があることに気付くでしょう。
これは、ハンドラーに async/await コードを実行させたい場合は、void メソッド内で await を実行する必要があることを意味します。これにより、疫病のように避けるように言われている、恐ろしい async void パターンが導入されます。
なぜ? async void は、例外が適切にバブルアップする機能を壊し、結果として多くの頭痛の種を引き起こす可能性があるためです。
私の意見では、単純な方が良いと思います。したがって、async void に関する以前の記事を読んで、目的が本当に EventHandler を処理することだけだった場合、これは役立つはずです。
前述の条件に基づいて、例外処理は async void の境界を越えて中断します。この境界を越えてバブルアップする必要がある例外がある場合は、楽しい時間を過ごすことができます。
おもしろいというのは、何かが機能しない理由をデバッグするのが好きで、何が壊れているのか明確な兆候がない場合は、本当に楽しい時間を過ごせるということです。
では、これを修正する最も簡単な方法は何ですか?
アクセスできる単純なツールである try/catch を使用して、例外が最初にこの境界を越えることができないようにしましょう。
objectThatRaisesEvent.TheEvent += async (s, e) => { // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up try { await SomeTaskYouWantToAwait(); } catch (Exception ex) { // TODO: put your exception handling stuff here } // no exception can escape here if the try/catch surrounds the entire handler body }
上記のコードで説明したように、イベント ハンドラーの本体全体に try/catch ブロックを配置すると、例外がその async void 境界を越えて発生するのを防ぐことができます。表面的には、これは非常に単純で、これを実装するのに特別なことは必要ありません。
長所:
短所:
そうは言っても、このソリューションは実にシンプルですが、もう少し改善できると思います。
最初に提案された解決策を改善できると私が考える 1 つの改善点は、例外のバブリングから安全な非同期 EventHandler があることをもう少し明確にすることができるということです。
このアプローチにより、時間の経過に伴うコードのドリフトによって、問題のあるコードがイベント ハンドラーの外部で実行されることも防止されます。ただし、これを手動で追加することを忘れないでください!
コードをチェックしてみましょう:
static class EventHandlers { public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Action<Exception> errorHandler) where TArgs : EventArgs => TryAsync<TArgs>( callback, ex => { errorHandler.Invoke(ex); return Task.CompletedTask; }); public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Func<Exception, Task> errorHandler) where TArgs : EventArgs { return new EventHandler<TArgs>(async (object s, TArgs e) => { try { await callback.Invoke(s, e); } catch (Exception ex) { await errorHandler.Invoke(ex); } }); } }
上記のコードは、まったく同じアプローチを使用して、例外が async void 境界を超えるのを防ぎます。イベント ハンドラーの本体をキャッチしようとしているだけですが、再利用するために明示的に専用のメソッドにまとめました。
これを適用すると、次のようになります。
someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>( async (s, e) => { Console.WriteLine("Starting the event handler..."); await SomeTaskToAwait(); Console.WriteLine("Event handler completed."); }, ex => Console.WriteLine($"[TryAsync Error Callback] Our exception handler caught: {ex}"));
これで、動作する非同期タスク シグネチャを持つデリゲートができたことがわかります。その中に入れたものはすべて、前に見たヘルパー メソッド内に try/catch があるので安心です。
以下は、例外を適切にキャプチャするエラー ハンドラ コールバックを示すスクリーンショットです。
長所:
短所:
もともと私は探検に着手しましたが、
この記事では、非同期 EventHandler を適切に動作させるための最も簡単な方法であり、(私の意見では) 洗練されたソリューションには、それを使用するために覚えておく必要があるという欠点しかありません。
コメント投稿者は、調査できることを示唆していました
コンパイル時の AoP フレームワークがいくつか存在しますが、それは読者の演習として残しておきます (これは、私がフォローアップするための演習でもあるためです)。