Cómo anular el registro correctamente de un controlador de eventos

En una revisión de código, tropecé con este fragmento de código (simplificado) para anular el registro de un controlador de eventos:

Fire -= new MyDelegate(OnFire); 

Pensé que esto no anula el registro del controlador de eventos porque crea un nuevo delegado que nunca se había registrado antes. Pero al buscar MSDN, encontré varias muestras de código que usan este modismo.

Entonces comencé un experimento:

 internal class Program { public delegate void MyDelegate(string msg); public static event MyDelegate Fire; private static void Main(string[] 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(string msg) { Console.WriteLine("OnFire: {0}", msg); } } 

Para mi sorpresa, sucedió lo siguiente:

  1. Fire("Hello 1"); produjo dos mensajes, como se esperaba.
  2. Fire("Hello 2"); ¡produjo un mensaje!
    ¡Esto me convenció de que el anular el registro de new delegates funciona!
  3. Fire("Hello 3"); lanzó una NullReferenceException .
    La depuración del código mostró que Fire es null después de anular el registro del evento.

Sé que para los controladores de eventos y delegates, el comstackdor genera una gran cantidad de código detrás de la escena. Pero todavía no entiendo por qué mi razonamiento es incorrecto.

¿Qué me estoy perdiendo?

Pregunta adicional: del hecho de que Fire es null cuando no hay eventos registrados, concluyo que en todos los lugares donde se dispara un evento, se requiere un chequeo contra null .

La implementación predeterminada del comstackdor de C # para agregar un controlador de eventos llama a Delegate.Combine , mientras elimina un controlador de eventos que llama a Delegate.Remove :

 Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire)); 

La implementación del Framework de Delegate.Remove no mira el objeto MyDelegate sí, sino el método al que se refiere el delegado ( Program.OnFire ). Por lo tanto, es perfectamente seguro crear un nuevo objeto MyDelegate al cancelar la suscripción de un controlador de eventos existente. Debido a esto, el comstackdor de C # le permite usar una syntax abreviada (que genera exactamente el mismo código detrás de las escenas) al agregar / eliminar manejadores de eventos: puede omitir la new MyDelegate parte new MyDelegate :

 Fire += OnFire; Fire -= OnFire; 

Cuando se elimina el último delegado del controlador de eventos, Delegate.Remove devuelve null. Como ha descubierto, es esencial comprobar el evento contra null antes de subirlo:

 MyDelegate handler = Fire; if (handler != null) handler("Hello 3"); 

Se asigna a una variable local temporal para defenderse contra una posible condición de carrera con la cancelación de la suscripción a manejadores de eventos en otros hilos. (Consulte la publicación de mi blog para obtener más información sobre la seguridad de la secuencia de asignación del controlador de eventos a una variable local). Otra forma de defenderse contra este problema es crear un delegado vacío que siempre esté suscrito; mientras esto usa un poco más de memoria, el controlador de eventos nunca puede ser nulo (y el código puede ser más simple):

 public static event MyDelegate Fire = delegate { }; 

Siempre debe verificar si un delegado no tiene objectives (su valor es nulo) antes de dispararlo. Como se dijo anteriormente, una forma de hacerlo es suscribirse con un método anónimo de no hacer nada que no se eliminará.

 public event MyDelegate Fire = delegate {}; 

Sin embargo, esto es solo un truco para evitar NullReferenceExceptions.

Simplemente verificar si un delegado es nulo antes de invocar no es inseguro, ya que otro subproceso puede anular el registro después de la verificación nula y hacer que sea nulo al invocar. Hay otra solución es copiar el delegado en una variable temporal:

 public event MyDelegate Fire; public void FireEvent(string msg) { MyDelegate temp = Fire; if (temp != null) temp(msg); } 

Desafortunadamente, el comstackdor JIT puede optimizar el código, eliminar la variable temporal y usar el delegado original. (según Juval Lowy – Progtwigción de componentes .NET)

Para evitar este problema, podría usar un método que acepte un delegado como parámetro:

 [MethodImpl(MethodImplOptions.NoInlining)] public void FireEvent(MyDelegate fire, string msg) { if (fire != null) fire(msg); } 

Tenga en cuenta que sin el atributo MethodImpl (NoInlining) el comstackdor JIT podría alinear el método haciéndolo inútil. Como los delegates son inmutables, esta implementación es insegura. Puedes usar este método como:

 FireEvent(Fire,"Hello 3");