Dans une revue de code, je suis tombé sur ce fragment de code (simplifié) pour désenregistrer un gestionnaire d’événement:
Fire -= new MyDelegate(OnFire);
Je pensais que cela ne désinsère pas le gestionnaire d’événements car il crée un nouveau délégué qui n’avait jamais été enregistré auparavant. Mais en recherchant MSDN, j’ai trouvé plusieurs exemples de code qui utilisent cet idiome.
J’ai donc commencé une expérience:
internal class Program { public delegate void MyDelegate(ssortingng msg); public static event MyDelegate Fire; private static void Main(ssortingng[] args) { Fire += new MyDelegate(OnFire); Fire += new MyDelegate(OnFire); Fire("Hello 1"); Fire -= new MyDelegate(OnFire); Fire("Hello 2"); Fire -= new MyDelegate(OnFire); Fire("Hello 3"); } private static void OnFire(ssortingng msg) { Console.WriteLine("OnFire: {0}", msg); } }
À ma grande surprise, les événements suivants se sont produits:
Fire("Hello 1");
produit deux messages, comme prévu. Fire("Hello 2");
produit un message! new
delegates fonctionne! Fire("Hello 3");
a lancé une NullReferenceException
. Fire
est null
après l’annulation de l’enregistrement de l’événement. Je sais que pour les gestionnaires d’événements et les delegates, le compilateur génère beaucoup de code derrière la scène. Mais je ne comprends toujours pas pourquoi mon raisonnement est faux.
Qu’est-ce que je rate?
Question supplémentaire: du fait que Fire
est null
quand il n’y a pas d’événements enregistrés, je conclus que partout où un événement est déclenché, une vérification par rapport à null
est requirejse.
L’implémentation par défaut du compilateur C # pour l’ajout d’un gestionnaire d’événements appelle Delegate.Combine
, tout en supprimant les appels de gestionnaire d’événements Delegate.Remove
:
Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
L’implémentation de Delegate.Remove
Framework ne regarde pas l’object MyDelegate
lui-même, mais la méthode à laquelle se réfère le délégué ( Program.OnFire
). Ainsi, il est parfaitement sûr de créer un nouvel object MyDelegate
lors du désabonnement d’un gestionnaire d’événement existant. De ce fait, le compilateur C # vous permet d’utiliser une syntaxe abrégée (qui génère exactement le même code en coulisse) lors de l’ajout / suppression de gestionnaires d’événements: vous pouvez omettre la new MyDelegate
partie new MyDelegate
:
Fire += OnFire; Fire -= OnFire;
Lorsque le dernier délégué est supprimé du gestionnaire d’événements, Delegate.Remove
renvoie null. Comme vous l’avez découvert, il est essentiel de vérifier l’événement contre null avant de le déclencher:
MyDelegate handler = Fire; if (handler != null) handler("Hello 3");
Il est assigné à une variable locale temporaire pour se défendre contre une éventuelle condition de concurrence avec le désabonnement des gestionnaires d’événements sur d’autres threads. (Voir l’article sur mon blog pour plus de détails sur la sécurité des threads concernant l’atsortingbution du gestionnaire d’événements à une variable locale.) Une autre façon de se défendre contre ce problème consiste à créer un délégué vide toujours abonné; Bien que cela utilise un peu plus de mémoire, le gestionnaire d’événements ne peut jamais être nul (et le code peut être plus simple):
public static event MyDelegate Fire = delegate { };
Vous devez toujours vérifier si un délégué n’a pas de cible (sa valeur est nulle) avant de le lancer. Comme nous l’avons déjà dit, une façon de faire est de s’abonner à une méthode anonyme qui ne sera pas supprimée.
public event MyDelegate Fire = delegate {};
Cependant, ce n’est qu’un hack pour éviter les NullReferenceExceptions.
Il suffit de regarder si un délégué est nul avant d’appeler n’est pas threadsafe car un autre thread peut se désinscrire après la vérification de null et le rendre nul lors de l’appel. Une autre solution consiste à copier le délégué dans une variable temporaire:
public event MyDelegate Fire; public void FireEvent(ssortingng msg) { MyDelegate temp = Fire; if (temp != null) temp(msg); }
Malheureusement, le compilateur JIT peut optimiser le code, éliminer la variable temporaire et utiliser le délégué d’origine. (selon Juval Lowy – Programmation de composants .NET)
Donc, pour éviter ce problème, vous pouvez utiliser la méthode qui accepte un délégué comme paramètre:
[MethodImpl(MethodImplOptions.NoInlining)] public void FireEvent(MyDelegate fire, ssortingng msg) { if (fire != null) fire(msg); }
Notez que sans l’atsortingbut MethodImpl (NoInlining), le compilateur JIT pourrait intégrer la méthode qui le rend inutile. Comme les delegates sont immuables, cette implémentation est threadsafe. Vous pouvez utiliser cette méthode comme:
FireEvent(Fire,"Hello 3");