c# - net - que es un evento de un control
Cómo anular el registro correctamente de un controlador de eventos (2)
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:
-
Fire("Hello 1");
produjo dos mensajes, como se esperaba. -
Fire("Hello 2");
¡produjo un mensaje!
¡Esto me convenció de que el anular el registro denew
delegados funciona! -
Fire("Hello 3");
lanzó unaNullReferenceException
.
La depuración del código mostró queFire
esnull
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 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 compilador 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 compilador de C # le permite usar una sintaxis 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 objetivos (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 compilador JIT puede optimizar el código, eliminar la variable temporal y usar el delegado original. (según Juval Lowy - Programació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 compilador JIT podría alinear el método haciéndolo inútil. Como los delegados son inmutables, esta implementación es insegura. Puedes usar este método como:
FireEvent(Fire,"Hello 3");