1. Introducción
Los eventos en C# no son simplemente variables donde puedes asignar un delegate. Son una lista protegida de manejadores, y solo el propietario del evento puede iniciar su ejecución; los demás solo pueden añadir (+=) o quitar (-=) sus reacciones.
Por ejemplo, así se puede suscribir a un evento:
worker.WorkCompleted += Worker_WorkCompleted;
A primera vista parece una suma normal, pero en realidad funciona distinto. Bajo el capó, el evento guarda una cadena de invocación (invocation list): un conjunto de delegates que hay que llamar cuando el evento se dispara. Cuando escribes +=, se añade un nuevo manejador a esa lista.
Vamos a ver con más detalle qué ocurre internamente, cómo se forma esa cadena y qué matices pueden surgir al suscribirse.
¿Qué es una cadena de delegates?
Recordemos: los delegates en C# son "multicast": pueden apuntar a varios métodos y todos ellos se ejecutarán en orden si se invoca el delegate. Los eventos usan este mecanismo: su valor es, en esencia, un delegate con una lista de manejadores.
En términos de código:
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
Cuando alguien se suscribe:
worker.WorkCompleted += MyHandler;
El compilador de C# bajo el capó hace algo parecido a esto:
- Toma el delegate actual (la lista de manejadores).
- Llama a Delegate.Combine para unir los manejadores.
- Escribe la cadena actualizada de vuelta en la variable del evento.
De forma esquemática:
| Operación | Lista interna de manejadores del evento |
|---|---|
| Antes | null o [Handler1] |
| Después de += Handler2 | [Handler1, Handler2] |
| Después de otro += H3 | [Handler1, Handler2, Handler3] |
Dato interesante: el mecanismo de multicast delegates no es "magia", es una implementación concreta: el delegate internamente contiene un array de métodos que hay que invocar.
2. Cómo funciona la suscripción: explicación sencilla
Veamos todo con un ejemplo concreto. Supongamos que tenemos un publicador (Worker) y subscribers (Logger, Notifier):
public class Worker
{
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
public void DoWork()
{
// ... trabajo ...
OnWorkCompleted("Zadanie zaversheno!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class Logger
{
public void LogWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Log: " + e.Message);
}
}
public class Notifier
{
public void ShowNotification(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Uvedomlenie: " + e.Message);
}
}
En Main:
var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;
// Inicio del trabajo
worker.DoWork();
Cuando se llama a OnWorkCompleted, el evento primero invoca logger.LogWorkCompleted, luego notifier.ShowNotification (en el orden en que te suscribiste).
3. Matices útiles
Visualización: cómo el evento guarda los manejadores
+---------------------+
| Worker |
|---------------------|
| WorkCompleted Event | ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+
Al suscribirse, cada nuevo manejador "se engancha" a la lista existente. Al disparar el evento, el delegate invoca cada método suscrito por orden.
Suscripción múltiple con el mismo método
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // ¡Dos veces!
En ese caso, el manejador se llamará tantas veces como esté suscrito — aquí, dos veces seguidas.
Suscripción con una lambda
worker.WorkCompleted += (sender, e) => Console.WriteLine("Anonymnyj obrabotchik: " + e.Message);
Si suscribes esa lambda varias veces — igual, se invocará varias veces por evento. Pero ojo: cada lambda es su propio objeto delegate, así que no puedes desuscribirla tan fácilmente (más detalles en la lección 260 y siguientes).
Cómo funciona internamente la suscripción: análisis a bajo nivel
Un evento es una propiedad especial con dos accesores (add/remove) que se llaman cuando usas += y -=. Si simplificamos, el compilador genera código parecido a:
// Más o menos así (simplificado)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
add { /* código para añadir el manejador */ }
remove { /* código para eliminar el manejador */ }
}
Por defecto se usa la implementación estándar: el delegate se combina con Delegate.Combine y se elimina con Delegate.Remove.
Esto protege el evento: desde fuera no se puede invocar el evento directamente (no hay worker.WorkCompleted(...);), solo suscribirse o desuscribirse.
Mecánica de la suscripción: esquema
+----------------------+
| |
v v
+--------------------+ +----------------------+
| LogWorkCompleted | | ShowNotification |
+--------------------+ +----------------------+
^ ^
\______________________/
^
|
WorkCompleted Event
La llamada "Invocation List" — la cadena de invocación.
Por qué importa: valor práctico
Entender la mecánica de la suscripción es clave para gestionar las conexiones entre objetos. Puedes construir sistemas complejos donde los componentes se suscriben y se desuscriben dinámicamente sin crear dependencias rígidas. Es un patrón estándar en UI frameworks, motores de juego, apps server y también en arquitecturas modernas (aunque en microservicios esto suele moverse a colas, la idea es similar).
En entrevistas preguntan a menudo cómo funciona el modelo de eventos en C#, por qué los eventos hacen el sistema flexible y cómo gestionar correctamente el ciclo de vida de las suscripciones.
4. Qué se puede (y no) hacer desde fuera de la clase
Se puede
- Suscribirse al evento (+=)
- Desuscribirse (-=)
No se puede
- Invocar el evento directamente
- Asignar un delegate al evento directamente (worker.WorkCompleted = ... — ¡error!)
Estas restricciones las impone la palabra clave event. Si declarases el delegate como un campo normal:
public EventHandler<WorkCompletedEventArgs> WorkCompleted; // ne event!
— cualquiera podría hacer de todo, incluso poner a null los manejadores, lo que causaría caos y bugs potenciales. Por eso casi siempre usa sólo event!
¿Se puede suscribir el mismo método a varios eventos?
¡Sí! Eso se llama "multisubscribe". Por ejemplo:
worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;
Si suscribiste el mismo método a eventos de distintos objetos, el manejador se disparará por ambos eventos. Dentro del manejador puedes saber quién disparó el evento por el parámetro sender.
Caso real: suscripciones dinámicas
Imagina una app donde el usuario lanza varias tareas en paralelo. Para cada tarea se crea un objeto Worker, y al evento de cada uno se suscribe el mismo manejador, por ejemplo logger.LogWorkCompleted.
var allWorkers = new List<Worker>();
for (int i = 0; i < 10; i++)
{
var w = new Worker();
w.WorkCompleted += logger.LogWorkCompleted;
allWorkers.Add(w);
}
Como resultado, cuando cualquiera de las tareas termine, el logger recibirá la notificación y registrará qué y cuándo pasó.
Aspectos prácticos: cómo ver los suscriptores de un evento
En código normal no puedes saber directamente cuántos manejadores hay en un evento (el evento encapsula el delegate). Sin embargo, dentro de la clase publicadora puedes acceder al delegate del evento, por ejemplo usando GetInvocationList():
// Sólo dentro de la clase publicadora
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
Console.WriteLine($"Podpischikov: {handlers.Length}");
Esto es útil si necesitas lógica de envío personalizada o debugging (aunque mejor usarlo sólo con fines educativos).
5. Errores típicos y particularidades
¿Manejador no agregado? ¡No se llamará!
Si no te suscribiste, el manejador nunca se ejecutará. Antes de invocar el evento siempre revisa por null (si no, obtendrás NullReferenceException).
Suscripción repetida
Si te suscribes varias veces al mismo evento con el mismo método, el manejador se ejecutará tantas veces como te hayas suscrito. A veces esto es una trampa: una llamada accidental extra a += convierte una sola notificación en varias iguales.
Métodos estáticos/instancia
Puedes suscribir métodos estáticos y de instancia. Lo importante es la firma correcta.
public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;
Lambdas y suscripción dentro de un bucle
Si, por ejemplo, en un bucle te suscribes con lambdas, asegúrate de entender qué variables captura la lambda. Puedes capturar valores distintos a los que esperas.
GO TO FULL VERSION