Un evento permite que una clase anuncie que algo ha ocurrido sin saber quién está escuchando. Otros objetos se suscriben con +=, y cuando la clase lanza el evento, se ejecuta el método de cada suscriptor.
Salida:
Order A-17 saved
receipt emailed for A-17
Order A-18 saved
receipt emailed for A-18
free gift added to A-18
Shop no sabe que existen los recibos ni los regalos. Lanza OrderPlaced, y reacciona lo que se haya suscrito. Añadir una reacción nueva (puntos de fidelidad, analítica) significa añadir un suscriptor, no editar PlaceOrder.
Declarar un evento
La declaración de un evento es un miembro con tipo delegado y la palabra clave event:
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
El tipo delegado fija la firma del manejador. El código .NET casi siempre usa uno de dos tipos incorporados, y seguir la convención hace que tus eventos resulten familiares a cualquier otro desarrollador de .NET:
EventHandler:void (object sender, EventArgs e), para eventos que no llevan datos. Lánzalo conEventArgs.Empty.EventHandler<TEventArgs>:void (object sender, TEventArgs e), dondeTEventArgses tu clase con los datos.
Las convenciones a su alrededor:
senderes el objeto que lanzó el evento, normalmentethis.- La clase de datos se llama
{Something}EventArgs, deriva deEventArgs(opcional desde .NET 4.5, pero todavía habitual) y expone propiedades de solo lectura. - El evento se nombra por lo que ocurrió:
OrderPlaced,Closed,PriceChanged. Un nombre en...ing(Closing) se usa para eventos que se lanzan antes de la acción, a menudo con una forma de cancelarla. - En las clases pensadas para heredarse, el lanzamiento pasa por un método
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e), para que las clases derivadas puedan engancharse.
Suscribirse y cancelar la suscripción
+= añade un manejador y -= lo quita. Un manejador puede ser un método o una lambda. Para quitar una lambda más tarde, guárdala en una variable, porque volver a escribir la misma lambda crea un delegado distinto que -= no encontrará:
Salida:
set to 32
display shows 32 C
alarm: too hot
set to 35
display shows 35 C
set to 20
display shows 20 C
Quitar un manejador que nunca se añadió no es un error; no hace nada. EventHandler<int> muestra que en .NET moderno el argumento de tipo no tiene que derivar de EventArgs, aunque una clase específica deja sitio para añadir campos más adelante sin romper a los suscriptores.
Los manejadores se ejecutan de forma síncrona, en el orden de suscripción, en el hilo que lanzó el evento. Si un manejador lanza una excepción, los manejadores restantes no se ejecutan y la excepción se propaga al código que lanzó el evento.
Lanzar un evento de forma segura
Un evento sin suscriptores contiene null. Llamarlo directamente lanzaría NullReferenceException, así que lánzalo con el operador condicional null:
OrderPlaced?.Invoke(this, args);
Antes de C# 6, el patrón era copiar el campo en una variable local, comprobarla e invocar la copia. La copia importa en código multihilo: comprobar OrderPlaced != null y después llamar a OrderPlaced(...) lee el campo dos veces, y otro hilo podría quitar el último manejador entre medias. ?. lo lee una sola vez, así que es más corto y además correcto.
Por qué event y no un campo delegado público
Sin la palabra clave event, un campo delegado público sirve para suscribirse, pero da a cada suscriptor el control total:
Salida:
analytics ping
analytics ping
Un = escrito en lugar de += eliminó en silencio los dos manejadores anteriores, y el código externo pudo lanzar el "clic" sin que hubiera ningún clic. Marcar el campo como event convierte las dos líneas en errores de compilación:
error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')
Dentro de Button, el evento sigue comportándose como un campo delegado normal, así que la clase puede invocarlo y comprobar si es null.
La fuga por no cancelar la suscripción
Cuando te suscribes, el evento guarda un delegado, y el delegado contiene una referencia al objeto suscriptor. Mientras el publicador esté vivo y el manejador siga enganchado, el recolector de basura no puede liberar al suscriptor, y este sigue recibiendo eventos cuando ya has terminado con él:
Salida:
closed widget shows 101.5
open widget shows 101.5
subscribers left: 1
closed widget shows 99.0
Poner leaky a null no cambió nada para el feed: su lista de manejadores sigue haciendo referencia a ese widget, así que el widget "cerrado" sigue actualizándose y sigue en memoria. El widget del bloque using canceló la suscripción en Dispose y dejó de recibir. Es una de las fugas de memoria más comunes en el código .NET de escritorio y de servidor, sobre todo con eventos static y servicios de toda la aplicación, que viven hasta que termina el proceso.
La regla: quien se suscribe a un evento de un objeto que vive más que él debe cancelar la suscripción, normalmente en Dispose (consulta la sentencia using). Cuando el publicador y el suscriptor tienen la misma vida, como un formulario y sus propios botones, no hace falta cancelar la suscripción.
Accesores add y remove propios
Un evento puede definir qué hacen += y -=, como el get y el set de una propiedad. Es raro, pero así es como los frameworks guardan los manejadores en un diccionario compartido o los reenvían a otro objeto:
private EventHandler closed;
public event EventHandler Closed
{
add { Console.WriteLine("subscriber added"); closed += value; }
remove { Console.WriteLine("subscriber removed"); closed -= value; }
}
Con accesores propios, la clase lanza el evento a través del campo de respaldo (closed?.Invoke(this, EventArgs.Empty)), ya que el evento en sí ya no tiene almacenamiento.
Preguntas frecuentes
¿Qué es un evento en C#?
Un evento es un miembro de una clase que permite que otros objetos pidan ser avisados cuando ocurre algo. Está respaldado por un delegado multicast, pero el código externo solo puede añadir manejadores (+=) y quitarlos (-=). Solo la clase que declara el evento puede lanzarlo.
¿Qué diferencia hay entre un evento y un delegado en C#?
Un delegado es un tipo que hace referencia a métodos. Un evento es un miembro declarado con un tipo delegado más la palabra clave event, que restringe el acceso: desde fuera de la clase no puedes invocarlo, leer su lista de manejadores ni asignarlo con =, solo suscribirte y cancelar la suscripción. Un campo delegado público permite todo eso, así que cualquier suscriptor podría borrar a los demás o disparar el evento.
¿Cómo paso datos con un evento de C#?
Deriva una clase de EventArgs con los datos como propiedades de solo lectura (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) y declara el evento como EventHandler<OrderPlacedEventArgs>. Lánzalo con OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.
¿Cómo lanzo un evento de forma segura en C#?
Usa MyEvent?.Invoke(this, args);. Un evento sin suscriptores es null, así que llamarlo directamente lanza NullReferenceException. El operador ?. lee el campo una sola vez, lo que además evita una condición de carrera en la que otro hilo quita el último manejador entre la comprobación de null y la llamada.
¿Pueden los eventos de C# provocar fugas de memoria?
Sí. Suscribirse guarda un delegado que hace referencia al suscriptor, así que el publicador mantiene vivo al suscriptor mientras el publicador viva. Un publicador de larga vida (un evento static, un servicio de toda la aplicación) con suscriptores de corta vida que nunca cancelan la suscripción los mantiene a todos en memoria. Cancela la suscripción con -= cuando el suscriptor termine, normalmente en Dispose.