Wenn wir über asynchrone EventHandler sprechen, fällt vielen von uns als Erstes ein, dass dies die einzige Ausnahme ist, die wir scheinbar zulassen
Als ich zuvor darüber geschrieben hatte, war ich begeistert, dass ich nach einer Lösung suchte, die darin bestand, die asynchrone Lücke tatsächlich existieren zu lassen (ohne mir den Rest der Haare ausreißen zu wollen).
Für mich ging es hier viel mehr um einige clevere Tricks, mit denen wir asynchrone EventHandler überwinden können, als darum, Lösungen bereitzustellen, um das Problem vollständig zu vermeiden.
Trotzdem fand der Artikel viel Anklang, wofür ich sehr dankbar bin, und einige Leute äußerten die Meinung, dass sie asynchrone EventHandler lieber auf andere Weise lösen würden.
Ich dachte, das wäre ein toller Punkt, deshalb wollte ich einen alternativen Ansatz entwickeln, der Async Void nicht behebt, sondern es Ihnen ermöglicht, ihn vollständig zu vermeiden (sehen Sie, was ich dort gemacht habe?), und gleichzeitig einige der Herausforderungen zu lösen mit asynchronen EventHandlern.
In diesem Artikel stelle ich eine weitere Lösung vor, die Sie in Ihrem eigenen Code ausprobieren können. Wir gehen auf die Vor- und Nachteile aus meiner Sicht hinsichtlich der Einsatzmöglichkeiten ein, damit Sie entscheiden können, ob es für Ihren Anwendungsfall sinnvoll ist.
Sie können auch interaktiven Code auf .NET fiddle right finden
Das Problem, mit dem wir bei asynchronen EventHandlern konfrontiert sind, besteht darin, dass die Signatur für Ereignisse, die wir in C# standardmäßig abonnieren können, etwa so aussieht:
void TheObject_TheEvent(object sender, EventArgs e);
Und Sie werden feststellen, dass wir durch die Angabe von „void“ am Anfang dieser Signatur gezwungen sind, „void“ in unseren eigenen Handlern zu verwenden, um das Ereignis zu abonnieren.
Das bedeutet, dass Sie, wenn Sie möchten, dass Ihr Handler jemals asynchronen/wartenden Code ausführt, in Ihrer void-Methode „wait“ verwenden müssen … Das führt zu dem großen, beängstigenden asynchronen Void-Muster, das wir wie die Pest vermeiden sollen.
Und warum? Weil async void die Fähigkeit von Ausnahmen beeinträchtigt, richtig aufzusteigen, und dadurch eine Menge Kopfschmerzen verursachen kann.
Meiner Meinung nach ist einfach besser. Wenn Sie also meinen vorherigen Artikel über async void gelesen haben und Ihr Ziel eigentlich nur darin bestand, mit EventHandlern umzugehen, sollte dies hilfreich sein.
Basierend auf den zuvor genannten Bedingungen bricht die Ausnahmebehandlung über die Grenze der asynchronen Lücke hinaus zusammen. Wenn Sie eine Ausnahme haben, die diese Grenze überschreiten muss, werden Sie eine lustige Zeit erleben.
Und mit Spaß meine ich, wenn Sie Spaß daran haben, zu debuggen, warum Dinge nicht funktionieren, und Sie keinen klaren Hinweis darauf haben, was kaputt ist, dann werden Sie wirklich eine tolle Zeit haben.
Was ist also der einfachste Weg, das Problem zu beheben?
Um zu verhindern, dass Ausnahmen diese Grenze überhaupt überschreiten, verwenden wir ein einfaches Tool, auf das wir Zugriff haben: 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 }
Wie im obigen Code erwähnt, können Sie verhindern, dass Ausnahmen über diese asynchrone Lückengrenze hinweg auftauchen, wenn Sie einen Try/Catch-Block um den GESAMTEN Hauptteil Ihres Ereignishandlers platzieren. Oberflächlich betrachtet ist es ganz einfach und erfordert nichts Besonderes, um es umzusetzen.
Vorteile:
Nachteile:
Trotzdem ist diese Lösung wirklich einfach, aber ich denke, wir können es noch ein bisschen besser machen.
Eine Verbesserung, die wir meiner Meinung nach gegenüber der ursprünglich vorgeschlagenen Lösung vornehmen können, besteht darin, dass wir etwas deutlicher machen können, dass wir einen asynchronen EventHandler haben, der vor der Entstehung von Ausnahmen geschützt sein sollte.
Dieser Ansatz verhindert auch, dass Codedrift im Laufe der Zeit dazu führt, dass problematischer Code außerhalb des Ereignishandlers ausgeführt wird. Es wird jedoch nicht auf die Tatsache eingegangen, dass Sie daran denken müssen, dies manuell hinzuzufügen!
Schauen wir uns den Code an:
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); } }); } }
Der obige Code verwendet im wahrsten Sinne des Wortes genau den gleichen Ansatz, um zu verhindern, dass Ausnahmen die Async-Void-Grenze überschreiten. Wir versuchen einfach, den Hauptteil des Event-Handlers zu erfassen, aber jetzt haben wir ihn in einer explizit dedizierten Methode zur Wiederverwendung gebündelt.
So würde die Anwendung aussehen:
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}"));
Wir können sehen, dass wir jetzt einen Delegaten mit einer asynchronen Task-Signatur haben, mit dem wir arbeiten können, und alles, was wir darin einfügen, wird, wie wir sicher sind, innerhalb der Hilfsmethode, die wir zuvor gesehen haben, einen Try/Catch-Umgang haben.
Hier ist ein Screenshot, der zeigt, wie der Rückruf des Fehlerhandlers die Ausnahme richtig erfasst:
Vorteile:
Nachteile:
Ursprünglich wollte ich die Gegend erkunden
In diesem Artikel haben wir untersucht, was meiner Meinung nach die einfachste Möglichkeit ist, dafür zu sorgen, dass sich Ihre asynchronen EventHandler ordnungsgemäß verhalten, und die verfeinerte Lösung (meiner Meinung nach) hat nur den Nachteil, dass Sie daran denken müssen, sie zu verwenden.
Ein Kommentator hatte vorgeschlagen, dass man es erkunden könnte
Es gibt einige AoP-Frameworks zur Kompilierungszeit, aber ich überlasse das als Übung für Sie als Leser (weil es auch eine Übung ist, die ich weiterverfolgen kann).