2008-11-15 20 views
59

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

Fire -= new MyDelegate(OnFire); 

pensé que esto no anular el registro del controlador de eventos, ya que 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.

así que empecé 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, ocurrió lo siguiente:

  1. Fire("Hello 1"); produjo dos mensajes, como se esperaba.
  2. Fire("Hello 2"); produjo un mensaje!
    Esto me convenció de que unregistering new delegados funciona!
  3. Fire("Hello 3"); lanzó un 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 delegados, el compilador 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 es Firenull cuando no hay eventos registrados, llego a la conclusión de que en todas partes se activa un evento, se requiere un cheque contra null.

Respuesta

78

implementación por defecto El C# del compilador de añadir un controlador de eventos llama Delegate.Combine, mientras que la eliminación de un controlador de eventos llama Delegate.Remove:

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

La implementación del Marco de Delegate.Remove no se fija en el objeto MyDelegate en sí, sino en 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 compilador de C# permite el uso de una sintaxis abreviada (que genera exactamente el mismo código detrás de las escenas) al añadir/eliminar controladores de eventos: se puede omitir la parte new MyDelegate:

Fire += OnFire; 
Fire -= OnFire; 

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

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

está asignada a una variable local temporal para defenderse de una posible condición de carrera con los controladores de eventos Dando de baja en otros hilos. (Consulte my blog post para obtener detalles sobre la seguridad de subprocesos al asignar el 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 que esta utiliza un poco más de memoria, el controlador de eventos no puede ser nulo (y el código puede ser más simple):

public static event MyDelegate Fire = delegate { }; 
+3

Sí. Para complementar: esto me confundió por un tiempo demasiado, sobre todo porque en C# que puede hacer Fire + = new MyDelegate (OnFire) o Fire + = OnFire; Este último parece ser más simple, pero es solo azúcar sintáctico para el primero. –

+0

@Nicholas Piasecki: Gracias; Actualicé mi respuesta para notar esa taquigrafía (bastante útil). –

+0

Puede valer la pena señalar que agregar un delegado de multidifusión y luego anular su suscripción puede generar resultados incorrectos si alguno de los métodos dentro de ese delegado de multidifusión también se suscribe y anula la suscripción individual. – supercat

15

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

public event MyDelegate Fire = delegate {}; 

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

El simple hecho de verificar si un delegado es nulo antes de invocar no es inseguro ya que otro subproceso puede anular el registro después de la nula y hacer que sea nulo al invocarlo. Hay una 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); 
} 

Por desgracia, el compilador JIT puede optimizar el código, eliminar la variable temporal, y usar el delegado originales. (Según Juval Lowy - Programación Componentes de .NET)

Así que para evitar este problema, se puede usar un método que acepta 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 la MethodImpl (NoInlining) atribuyen el JIT el compilador podría alinear el método haciéndolo inútil. Dado que los delegados son inmutables, esta implementación es segura para los hilos. Puede usar este método como:

FireEvent(Fire,"Hello 3"); 
+0

Muchas gracias por aclarar los problemas de JIT. Trampas y trampas en todas partes. – gyrolf

+7

En realidad, esto no es posible con Microsoft CLR 2.0, debido a su modelo de memoria más fuerte: http://code.logos.com/blog/2008/11/events_and_threads_part_4.html –

Cuestiones relacionadas