Delegados y Eventos en C#
Aprende delegados y eventos en C# para crear aplicaciones basadas en eventos con callbacks y ejemplos prácticos.
En C#, los delegates y los events constituyen la base de la programación orientada a eventos.
Un delegate es una referencia segura en tipos (type-safe) que puede apuntar a un método.
Un event permite que una clase notifique al mundo exterior que “una determinada acción ha ocurrido”.
Estos mecanismos permiten construir arquitecturas débilmente acopladas (loosely coupled)
y hacen posible ejecutar distintos métodos de forma dinámica.
¿Qué es un Delegate?
Un delegate es un tipo que puede contener referencias a métodos con una firma específica (parámetros y tipo de retorno). En otras palabras, permite tratar los métodos como si fueran variables.
Una variable delegate puede apuntar a un método compatible e invocarlo dinámicamente. Esto permite escribir código flexible y débilmente acoplado.
// Definición de un delegate
public delegate void Notify(string message);
class Program
{
static void SendEmail(string message)
{
Console.WriteLine("Email enviado: " + message);
}
static void Main()
{
Notify notifyHandler;
// Asignar un método al delegate
notifyHandler = SendEmail;
notifyHandler("La reunión es a las 10:00 AM.");
}
}
En este ejemplo, el delegate notifyHandler
hace referencia al método SendEmail y lo ejecuta.
Delegates Multicast
Un delegate multicast es un delegate que puede referenciar múltiples métodos al mismo tiempo.
En C#, los delegates se vuelven multicast cuando se combinan usando el operador +=.
Cuando se invoca un delegate multicast, cada método en la lista de invocación se ejecuta en el orden en que fue agregado. Esto es especialmente útil para enviar una notificación a múltiples suscriptores.
Ejemplo
using System;
class Program
{
delegate void Notify(string message);
static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile;
notifier += SendEmail;
notifier("Build completado.");
}
static void LogToConsole(string message)
{
Console.WriteLine("[Console] " + message);
}
static void LogToFile(string message)
{
Console.WriteLine("[Archivo] " + message);
}
static void SendEmail(string message)
{
Console.WriteLine("[Email] " + message);
}
}
Un método puede eliminarse de la lista de invocación usando el operador -=:
notifier -= SendEmail;
- Los delegates multicast se utilizan internamente en los events.
- Si uno de los manejadores lanza una excepción, la ejecución se detiene.
- Si el delegate tiene un tipo de retorno, solo se devuelve el resultado del último método.
Comportamiento del Valor de Retorno
Si un delegate multicast tiene un tipo de retorno, todos los métodos de la lista de invocación se ejecutan, pero solo se devuelve el resultado del último método.
using System;
class Program
{
delegate int Calculate(int x);
static void Main()
{
Calculate calc = Square;
calc += Double;
int result = calc(5);
Console.WriteLine(result);
// Resultado: 10 (resultado del último método: Double)
}
static int Square(int x)
{
Console.WriteLine("Square llamado");
return x * x;
}
static int Double(int x)
{
Console.WriteLine("Double llamado");
return x * 2;
}
}
Tanto Square como Double se ejecutan;
sin embargo, el delegate devuelve únicamente el resultado del último método agregado.
- Todos los métodos se ejecutan en orden.
- Solo se conserva el valor de retorno del último método.
- Por esta razón, los delegates multicast suelen utilizarse con tipo de retorno
void(como en los events).
¿Qué es un Event?
Un event es un mecanismo que señala que una acción ha ocurrido.
Los events están construidos sobre delegates y proporcionan acceso controlado.
La palabra clave event se utiliza para su declaración,
y desde el exterior solo puede accederse mediante += (suscribirse)
o -= (cancelar suscripción).
Esto garantiza que:
- El event solo puede activarse dentro de la clase que lo declara.
- El código externo no puede sobrescribir la lista de invocación.
- Se preserve la encapsulación.
public class Button
{
// Definición del event (EventHandler es un delegate estándar)
public event EventHandler? Click;
public void SimulateClick()
{
Console.WriteLine("¡Botón clicado!");
// Disparar el event
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var btn = new Button();
// Suscribirse al event
btn.Click += (s, e) => Console.WriteLine("Event: El botón fue clicado.");
btn.SimulateClick();
}
}
El operador condicional nulo (?.) garantiza que el event
solo se invoque si existen suscriptores,
evitando una NullReferenceException.
En este ejemplo, la clase Button publica un event Click.
La expresión lambda suscrita se ejecuta cuando el event se dispara.
Este patrón se utiliza ampliamente en frameworks UI como WinForms y WPF.
EventHandler y EventArgs
En C#, el delegate estándar más utilizado para events es
EventHandler.
Sigue el patrón tradicional:
(object sender, EventArgs e).
Al derivar de EventArgs,
es posible enviar datos adicionales relacionados con el event.
Este enfoque sigue las directrices oficiales de diseño de events en .NET.
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(int id, decimal amount)
{
Console.WriteLine($"Pedido creado (ID={id}, Importe={amount})");
// Disparar el event
OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
}
}
class Program
{
static void Main()
{
var service = new OrderService();
service.OrderCreated += (s, e) =>
{
Console.WriteLine($"Notificación: Pedido recibido (#{e.OrderId}, {e.Amount} USD)");
};
service.CreateOrder(101, 250m);
}
}
En este ejemplo, la clase OrderService dispara el event
OrderCreated cuando se crea un nuevo pedido.
El suscriptor recibe los detalles del pedido a través de la clase personalizada
OrderEventArgs.
Este enfoque garantiza seguridad de tipos y hace que la comunicación orientada a eventos sea clara y estructurada.
Delegate vs Event (Encapsulación)
Aunque los events están construidos sobre delegates, no son lo mismo. La diferencia principal radica en la encapsulación y el control de acceso.
Un campo delegate puede modificarse libremente desde fuera de la clase, mientras que un event restringe el acceso externo y protege la lista de invocación.
Ejemplo con Delegate
public class Publisher
{
public Action? OnChange;
}
Como OnChange es un campo delegate:
- El código externo puede asignarlo usando
=. - Todos los suscriptores existentes pueden eliminarse accidentalmente.
- Puede establecerse en
null. - Puede invocarse directamente desde el exterior.
publisher.OnChange = null; // Elimina todos los suscriptores
publisher.OnChange = SomeMethod; // Reemplaza la lista de invocación
publisher.OnChange?.Invoke(); // Puede invocarse externamente
Ejemplo con Event
public class Publisher
{
public event Action? OnChange;
}
Como OnChange está declarado como un event:
- El código externo solo puede usar
+=y-=. - No puede sobrescribir la lista de invocación con
=. - No puede invocar el event directamente.
- El event solo puede activarse dentro de la clase que lo declara.
- Los delegates proporcionan flexibilidad.
- Los events proporcionan acceso controlado y encapsulación.
- Para notificaciones públicas, casi siempre se deben preferir events en lugar de campos delegate.
Alternativas modernas: Action & Func
En C# moderno, definir tipos delegate personalizados suele ser innecesario.
En su lugar, puedes utilizar los delegates genéricos integrados
Action y Func.
Estos delegates son flexibles, concisos y ampliamente utilizados en el ecosistema .NET (incluyendo LINQ, programación asíncrona y APIs basadas en tareas).
Action
Action representa un método que no devuelve ningún valor.
Puede aceptar cero o más parámetros.
Action log = message => Console.WriteLine(message);
log("Application started.");
Func
Func<T, TResult> representa un método que devuelve un valor.
El último parámetro genérico siempre representa el tipo de retorno.
Func add = (a, b) => a + b;
int result = add(5, 3); // 8
Actionse utiliza cuando no se necesita valor de retorno.Funcse utiliza cuando se requiere un valor de retorno.- El uso de estos delegates integrados reduce el código repetitivo.
- La mayoría de los proyectos C# modernos prefieren
ActionyFuncen lugar de delegates personalizados.
Cancelación de suscripción y fugas de memoria
Un aspecto importante al trabajar con events es la cancelación de suscripción. Si te suscribes a un event y nunca te desuscribes, puede provocar fugas de memoria inesperadas.
Los events mantienen una referencia fuerte a sus suscriptores. Mientras el objeto publisher esté vivo, el subscriber no podrá ser recolectado por el garbage collector.
¿Por qué es un problema?
Imagina un publisher de larga duración (por ejemplo, un servicio o singleton) y un subscriber de corta duración (por ejemplo, un componente UI). Si el subscriber no se desuscribe, permanecerá en memoria incluso cuando ya no sea necesario.
public class Publisher
{
public event Action? OnChange;
public void Raise()
{
OnChange?.Invoke();
}
}
public class Subscriber
{
private readonly Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.OnChange += HandleChange;
}
private void HandleChange()
{
Console.WriteLine("Change detected.");
}
public void Dispose()
{
// Importante: ¡cancelar suscripción!
_publisher.OnChange -= HandleChange;
}
}
Si Dispose nunca se llama,
la instancia Subscriber seguirá referenciada por el event
y no podrá ser recolectada por el garbage collector.
- Siempre desuscríbete de los events cuando termine la vida útil del subscriber.
- Esto es especialmente importante en frameworks UI (WinForms, WPF, Blazor, etc.).
- Publisher de larga duración + subscriber de corta duración = posible fuga de memoria.
- Implementar
IDisposablees una solución común para una desuscripción segura.
Seguridad en hilos y buenas prácticas
Al trabajar con events en entornos multihilo,
la seguridad en hilos se vuelve importante.
Un problema común ocurre al verificar null
antes de invocar un event.
Posible condición de carrera
if (OnChange != null)
{
OnChange(this, EventArgs.Empty);
}
Entre la verificación de null y la invocación,
otro hilo podría cancelar la suscripción del event.
Esto puede provocar una NullReferenceException.
Patrón de invocación seguro
Para evitar esta condición de carrera, copia la referencia del delegate en una variable local antes de invocarlo:
var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);
Esto garantiza que la lista de invocación no cambie entre la verificación y la llamada al método.
Sintaxis moderna
En la mayoría de los casos, el operador condicional nulo ofrece una forma concisa y segura de invocar events:
OnChange?.Invoke(this, EventArgs.Empty);
- Utiliza patrones de invocación seguros en escenarios multihilo.
- Prefiere
?.Invokepara una sintaxis moderna y limpia. - Los events son delegates multicast — un manejador que falle puede detener la ejecución.
- Considera aislar las llamadas con try/catch si la confiabilidad es crítica.
¿Cuándo usar Delegates y Events?
-
Delegates:
Úsalos para pasar métodos como parámetros,
implementar callbacks o crear flujos de ejecución flexibles.
En C# moderno, normalmente se prefieren
ActionyFunc. - Events: Úsalos cuando una clase deba exponer notificaciones preservando la encapsulación. Son ideales para interacciones UI, eventos de dominio y cambios de estado.
- Buena práctica: Utiliza events para notificaciones públicas y evita exponer directamente campos delegate. Desuscríbete cuando sea necesario y aplica patrones de invocación seguros.
TL;DR
delegate: Referencias seguras en tipos a métodos con soporte multicast.event: Delegate controlado que permite suscripción y preserva la encapsulación.EventHandleryEventArgs: Siguen el patrón estándar de events en .NET para pasar datos.ActionyFunc: Alternativas modernas a delegates personalizados.- Desuscríbete de los events cuando sea necesario para evitar fugas de memoria.
- Usa events para notificaciones y delegates (o
Action/Func) para callbacks y control de flujo.
Artículos relacionados
Clases, Objetos, Propiedades y Métodos en C#
Aprende cómo las clases, objetos, propiedades y métodos en C# forman la base de la programación orientada a objetos.
Expresiones Lambda en C#
Aprende expresiones lambda en C#, incluyendo sintaxis concisa, delegados Func y Action y ejemplos prácticos con LINQ.
Interfaces y Clases Abstractas en C#
Aprende interfaces y clases abstractas en C#, sus diferencias y cuándo usar cada una para diseñar código limpio y extensible.
Métodos y uso de parámetros en C#
Aprende a definir métodos y usar parámetros en C#, incluyendo parámetros por valor y referencia, parámetros opcionales y ejemplos.