Lorsque nous discutons des gestionnaires d'événements asynchrones, la première chose qui vient à l'esprit pour beaucoup d'entre nous est que c'est la seule exception que nous semblons autoriser
Quand j'avais écrit à ce sujet auparavant, j'étais ravi d'explorer une solution qui impliquait de permettre au vide asynchrone d'exister (sans vouloir arracher le reste de mes cheveux).
Pour moi, il s'agissait bien plus de quelques astuces astucieuses que nous pouvons utiliser pour surmonter les EventHandlers asynchrones que de fournir des solutions pour éviter complètement le problème.
Cela dit, l'article a suscité beaucoup d'intérêt, ce dont je suis très reconnaissant, et certaines personnes ont exprimé leur opinion selon laquelle elles préféreraient résoudre les EventHandlers asynchrones d'une manière différente.
J'ai pensé que c'était un bon point, alors je voulais proposer une approche alternative qui ne corrige pas le vide asynchrone, mais qui vous permet de l'éviter (vous voyez ce que j'ai fait là-bas ?) tout en résolvant certains des défis avec des gestionnaires d'événements asynchrones.
Dans cet article, je vais présenter une autre solution que vous pouvez essayer dans votre propre code. Nous aborderons les avantages et les inconvénients de mon point de vue en ce qui concerne la façon dont il peut être utilisé afin que vous puissiez décider si cela a du sens pour votre cas d'utilisation.
Vous pouvez également trouver du code interactif sur le violon .NET à droite
Le problème auquel nous sommes confrontés avec les gestionnaires d'événements asynchrones est que la signature des événements auxquels nous pouvons nous abonner en C# ressemble par défaut à ceci :
void TheObject_TheEvent(object sender, EventArgs e);
Et, vous remarquerez qu'en ayant void au début de cette signature, nous sommes obligés d'utiliser void dans nos propres gestionnaires afin de souscrire à l'événement.
Cela signifie que si vous voulez que votre gestionnaire exécute du code async/wait, vous devrez attendre dans votre méthode void… Ce qui introduit le grand motif void async effrayant qu'on nous dit d'éviter comme la peste.
Et pourquoi? Parce que le vide asynchrone empêche les exceptions de se propager correctement et peut en conséquence causer une tonne de maux de tête.
À mon avis, la simplicité est préférable… donc si vous avez lu mon article précédent sur le vide asynchrone et que votre objectif était vraiment de gérer les EventHandlers, cela devrait vous aider.
Sur la base des conditions énoncées précédemment, la gestion des exceptions échoue au-delà de la limite du vide asynchrone. Si vous avez une exception qui doit franchir cette frontière, vous passerez un bon moment.
Et par plaisir, je veux dire si vous aimez déboguer pourquoi les choses ne fonctionnent pas et que vous n'avez pas d'indication claire sur ce qui ne va pas, alors vous passerez vraiment un bon moment.
Alors, quel est le moyen le plus simple de résoudre ce problème ?
Empêchez les exceptions de franchir cette limite en premier lieu en utilisant un outil simple auquel nous avons accès : 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 }
Comme indiqué dans le code ci-dessus, si vous placez un bloc try/catch autour du corps ENTIER de votre gestionnaire d'événements, vous pouvez empêcher toute exception de se propager à travers cette limite vide asynchrone. En surface, c'est assez simple et ne nécessite rien d'extraordinaire pour l'implémenter.
Avantages:
Les inconvénients:
Cela dit, cette solution est vraiment simple, mais je pense que nous pouvons faire un peu mieux.
Une amélioration que je pense que nous pouvons apporter par rapport à la solution initialement proposée est que nous pouvons rendre un peu plus explicite le fait que nous avons un EventHandler asynchrone qui devrait être à l'abri des exceptions.
Cette approche empêchera également la dérive du code au fil du temps de provoquer l'exécution de code problématique en dehors du gestionnaire d'événements. Cependant, cela n'abordera pas le fait que vous devez vous rappeler de l'ajouter manuellement !
Vérifions le code :
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); } }); } }
Le code ci-dessus utilise littéralement exactement la même approche pour empêcher les exceptions de franchir la limite vide asynchrone. Nous essayons simplement de comprendre le corps du gestionnaire d'événements, mais nous l'avons maintenant regroupé dans une méthode explicitement dédiée à réutiliser.
Voici à quoi cela ressemblerait pour l'appliquer:
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}"));
Nous pouvons voir que nous avons maintenant un délégué avec une signature de tâche asynchrone avec laquelle travailler, et tout ce que nous mettons à l'intérieur de celui-ci, nous sommes assurés qu'il y aura un try/catch autour de lui dans la méthode d'assistance que nous avons vue précédemment.
Voici une capture d'écran montrant le rappel du gestionnaire d'erreurs capturant correctement l'exception :
Avantages:
Les inconvénients:
Alors qu'à l'origine j'avais entrepris d'explorer
Dans cet article, nous avons exploré ce que je pourrais considérer comme le moyen le plus simple de faire en sorte que vos gestionnaires d'événements asynchrones se comportent correctement, et la solution raffinée (à mon avis) n'a que l'inconvénient que vous devez vous rappeler de l'utiliser.
Un commentateur avait suggéré que l'on pourrait explorer
Il existe des frameworks AoP au moment de la compilation, mais je vais laisser cela comme un exercice pour vous en tant que lecteur (car c'est aussi un exercice pour moi de suivre).