Delegados y Eventos en C#: Construyendo Arquitecturas Flexibles y Reactivas
Este tutorial te guiará a través del uso de delegados y eventos en C#, componentes fundamentales para el diseño de arquitecturas de software flexibles y reactivas. Aprenderás a desacoplar tus componentes, manejar notificaciones y construir sistemas más robustos y mantenibles. Exploraremos desde los conceptos básicos hasta su aplicación en escenarios complejos.
Los delegados y eventos son pilares en la programación C# que permiten un diseño de software desacoplado y orientado a eventos. Son herramientas esenciales para construir sistemas donde los componentes pueden comunicarse sin conocer directamente las implementaciones de los demás, facilitando la extensibilidad y el mantenimiento.
En este tutorial, profundizaremos en cómo funcionan, cómo implementarlos y cuándo utilizarlos para escribir código más limpio y eficiente. ¡Prepárate para llevar tus habilidades en C# al siguiente nivel! ✨
🚀 Introducción a los Delegados en C#
Imagina que quieres pasar un método como argumento a otro método, o almacenar una referencia a un método para invocarlo más tarde. Aquí es donde entran los delegados.
Un delegado es un tipo que encapsula un método con una firma particular (tipo de retorno y parámetros). Piensa en él como un puntero a función seguro y orientado a objetos. Es una forma de decir: "Oye, aquí hay una referencia a un método que se ve así, y puedes llamarlo más tarde."
💡 ¿Por qué son importantes los delegados?
Los delegados son la base para muchas características clave de C#, incluyendo:
- Eventos: Como veremos, los eventos se construyen sobre delegados.
- Callbacks: Permiten definir funciones que se ejecutarán cuando algo suceda.
- Programación asíncrona: Utilizados en muchas operaciones asíncronas para notificar la finalización.
- LINQ: Aunque no directamente visible, los delegados están detrás de la magia de las expresiones lambda en LINQ.
🎯 Declaración y Uso de Delegados
Para usar un delegado, primero debes declararlo. La declaración de un delegado define la firma de los métodos que puede encapsular.
Sintaxis de Declaración de un Delegado
public delegate int MiDelegadoDeCalculo(int a, int b);
En este ejemplo:
public delegate: Palabras clave para declarar un delegado.int: Tipo de retorno de los métodos que este delegado puede encapsular.MiDelegadoDeCalculo: El nombre de nuestro delegado (es una convención de C# que los nombres de los delegados terminen enDelegateo, como en este caso, se refieran a su propósito).(int a, int b): Los parámetros que deben tener los métodos que este delegado puede encapsular.
Ejemplo Práctico de Delegados
Veamos un ejemplo completo:
using System;
// 1. Declaración del delegado
public delegate int OperacionAritmetica(int a, int b);
public class Calculadora
{
// Métodos que coinciden con la firma del delegado
public static int Sumar(int x, int y)
{
return x + y;
}
public static int Restar(int x, int y)
{
return x - y;
}
public int Multiplicar(int x, int y)
{
return x * y;
}
}
public class Programa
{
public static void Main(string[] args)
{
// 2. Creación de instancias del delegado
// Se puede asignar un método estático o de instancia.
OperacionAritmetica delegadoSuma = new OperacionAritmetica(Calculadora.Sumar);
OperacionAritmetica delegadoResta = Calculadora.Restar; // Sintaxis abreviada
Calculadora calc = new Calculadora();
OperacionAritmetica delegadoMultiplica = new OperacionAritmetica(calc.Multiplicar);
// 3. Invocación del delegado
Console.WriteLine($"Suma: {delegadoSuma(10, 5)}"); // Output: Suma: 15
Console.WriteLine($"Resta: {delegadoResta.Invoke(10, 5)}"); // Output: Resta: 5 (otra forma de invocar)
Console.WriteLine($"Multiplicación: {delegadoMultiplica(10, 5)}"); // Output: Multiplicación: 50
// Los delegados pueden apuntar a cualquier método con la firma correcta
OperacionAritmetica delegadoActual = Calculadora.Sumar;
Console.WriteLine($"Resultado actual: {delegadoActual(20, 10)}"); // Output: Resultado actual: 30
delegadoActual = Calculadora.Restar;
Console.WriteLine($"Resultado actual: {delegadoActual(20, 10)}"); // Output: Resultado actual: 10
}
}
✅ Multicast Delegates
Una característica poderosa de los delegados es que pueden encapsular múltiples métodos. Esto se conoce como multicast delegates. Cuando invocas un delegado de multidifusión, todos los métodos encapsulados se ejecutan en el orden en que se agregaron.
Puedes usar los operadores + y += para añadir métodos, y - y -= para quitarlos.
using System;
public delegate void NotificadorMensaje(string mensaje);
public class ServicioLog
{
public static void LogConsola(string msg)
{
Console.WriteLine($"[LOG CONSOLA]: {msg}");
}
public static void LogArchivo(string msg)
{
Console.WriteLine($"[LOG ARCHIVO simulado]: {msg}");
}
}
public class ProgramaMulticast
{
public static void Main(string[] args)
{
NotificadorMensaje miNotificador = null;
// Añadir métodos al delegado
miNotificador += ServicioLog.LogConsola;
miNotificador += ServicioLog.LogArchivo;
Console.WriteLine("\n--- Invocando Notificador con dos métodos ---");
miNotificador?.Invoke("¡Operación completada con éxito!");
// Quitar un método del delegado
miNotificador -= ServicioLog.LogConsola;
Console.WriteLine("\n--- Invocando Notificador con un método (Consola removido) ---");
miNotificador?.Invoke("Solo se registrará en archivo.");
// Si un delegado devuelve un valor y es multicast, solo se devuelve el valor del último método invocado.
// Por eso, los delegados multicast suelen ser void.
}
}
📖 Delegados Genéricos Predefinidos: Action y Func
Microsoft ha simplificado el uso de delegados con dos tipos genéricos predefinidos: Action y Func. ¡Estos son los que usarás la mayor parte del tiempo!
Action
Un delegado Action encapsula un método que no devuelve ningún valor (void). Puede tomar de 0 a 16 parámetros de entrada.
Action: Sin parámetros,voidde retorno.Action<T>: Un parámetro de tipoT,voidde retorno.Action<T1, T2>: Dos parámetros de tipoT1yT2,voidde retorno.
Ejemplo:
using System;
public class ProgramaAction
{
public static void Saludar(string nombre)
{
Console.WriteLine($"¡Hola, {nombre}!");
}
public static void MostrarHora()
{
Console.WriteLine($"La hora actual es: {DateTime.Now.ToShortTimeString()}");
}
public static void Main(string[] args)
{
Action<string> accionSaludar = Saludar;
accionSaludar("Mundo"); // Output: ¡Hola, Mundo!
Action accionMostrarHora = MostrarHora;
accionMostrarHora(); // Output: La hora actual es: [hora actual]
// Con expresiones lambda
Action<int, int> sumarImprimir = (a, b) => Console.WriteLine($"Suma (lambda): {a + b}");
sumarImprimir(5, 3);
}
}
Func
Un delegado Func encapsula un método que devuelve un valor. El último parámetro genérico es siempre el tipo de retorno. Puede tomar de 0 a 16 parámetros de entrada.
Func<TResult>: Sin parámetros, devuelveTResult.Func<T, TResult>: Un parámetro de tipoT, devuelveTResult.Func<T1, T2, TResult>: Dos parámetros de tipoT1yT2, devuelveTResult.
Ejemplo:
using System;
public class ProgramaFunc
{
public static int ObtenerNumeroAleatorio()
{
return new Random().Next(1, 100);
}
public static bool EsMayorQueDiez(int numero)
{
return numero > 10;
}
public static void Main(string[] args)
{
Func<int> obtenerRandom = ObtenerNumeroAleatorio;
Console.WriteLine($"Número aleatorio: {obtenerRandom()}");
Func<int, bool> esMayor = EsMayorQueDiez;
Console.WriteLine($"¿Es 5 mayor que 10? {esMayor(5)}"); // Output: False
Console.WriteLine($"¿Es 15 mayor que 10? {esMayor(15)}"); // Output: True
// Con expresiones lambda
Func<string, string, string> concatenar = (s1, s2) => s1 + " " + s2;
Console.WriteLine($"Concatenado (lambda): {concatenar("Hola", "Mundo")}");
}
}
🔗 ¿Qué son los Eventos en C#?
Los eventos son una característica de lenguaje que proporciona una forma de notificación para los objetos. Permiten que una clase o un objeto (el publicador) notifique a otras clases o objetos (los suscriptores) cuando algo interesante sucede, sin que el publicador tenga que saber nada sobre los suscriptores. Esto promueve un diseño desacoplado y es fundamental para la programación orientada a eventos.
Los eventos en C# se construyen sobre delegados.
Componentes Clave de los Eventos
- Publicador (Publisher): La clase que contiene el evento y lo 'dispara' (lo eleva) cuando ocurre algo.
- Suscriptor (Subscriber): La clase que contiene el método que se ejecutará en respuesta al evento.
- Delegado: Define la firma de los métodos que pueden suscribirse al evento.
- Evento: Es una instancia de un delegado, declarada con la palabra clave
event.
⚡ Declaración de un Evento
Un evento se declara usando la palabra clave event y un tipo de delegado. La convención estándar de .NET para los eventos es usar EventHandler o EventHandler<TEventArgs>.
EventHandler
Se usa para eventos que no necesitan pasar datos adicionales. Su firma es:
public delegate void EventHandler(object sender, EventArgs e);
sender: El objeto que originó el evento (el publicador).e: Un objeto de tipoEventArgsque contiene datos específicos del evento (vacío si no hay datos).
EventHandler<TEventArgs>
Se usa cuando necesitas pasar datos específicos del evento. TEventArgs debe ser una clase que herede de EventArgs.
Su firma es:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
sender: El objeto que originó el evento.e: Un objeto de tu clase personalizadaTEventArgscon los datos del evento.
Pasos para Implementar Eventos
- Definir
EventArgs(si se necesitan datos): Crea una clase que herede deSystem.EventArgspara contener cualquier dato relevante para el evento. - Declarar el Delegado: Generalmente, usas
EventHandleroEventHandler<TEventArgs>. - Declarar el Evento: En la clase publicadora, declara el evento usando la palabra clave
eventy el tipo de delegado. - Disparar el Evento: En la clase publicadora, crea un método (
On[NombreEvento]) que eleve el evento. Asegúrate de comprobar si hay suscriptores (?.Invoke()). - Suscribirse al Evento: En la clase suscriptora, crea un método (el manejador del evento) con la firma del delegado y suscríbete al evento usando el operador
+=. - Desuscribirse del Evento: Cuando ya no sea necesario, desuscríbete usando el operador
-=para evitar memory leaks.
🛠️ Ejemplo Completo de Delegados y Eventos
Vamos a crear un sistema simple donde un Reloj (publicador) notifica a Pantalla y Registrador (suscriptores) cada segundo.
1. Definir EventArgs (si aplica)
En este caso, simplemente notificaremos, así que EventArgs.Empty será suficiente. Si quisiéramos pasar la hora exacta, haríamos algo así:
// public class HoraEventArgs : EventArgs
// {
// public DateTime HoraActual { get; }
// public HoraEventArgs(DateTime hora) { HoraActual = hora; }
// }
2. Clase Publicadora: Reloj
using System;
using System.Threading;
public class Reloj
{
// 2. Declarar el Delegado (usando EventHandler predefinido)
// public delegate void SegundoTranscurridoEventHandler(object sender, EventArgs e); // Opcional si usas EventHandler
// 3. Declarar el Evento
public event EventHandler SegundoTranscurrido;
public void Iniciar()
{
Console.WriteLine("Reloj iniciado. Presiona cualquier tecla para detenerlo.");
while (!Console.KeyAvailable)
{
Thread.Sleep(1000); // Espera 1 segundo
OnSegundoTranscurrido(); // Dispara el evento
}
Console.WriteLine("Reloj detenido.");
}
// 4. Método para disparar el evento (convención: On[NombreEvento])
protected virtual void OnSegundoTranscurrido()
{
// El operador ?.Invoke() es una forma segura de invocar el evento
// Solo se invocará si hay al menos un suscriptor.
SegundoTranscurrido?.Invoke(this, EventArgs.Empty);
// Si usáramos HoraEventArgs: SegundoTranscurrido?.Invoke(this, new HoraEventArgs(DateTime.Now));
}
}
3. Clases Suscriptoras: Pantalla y Registrador
using System;
public class Pantalla
{
public void Suscribirse(Reloj reloj)
{
reloj.SegundoTranscurrido += ManejarSegundoTranscurrido;
Console.WriteLine("Pantalla: Suscrita al evento SegundoTranscurrido.");
}
public void Desuscribirse(Reloj reloj)
{
reloj.SegundoTranscurrido -= ManejarSegundoTranscurrido;
Console.WriteLine("Pantalla: Desuscrita del evento SegundoTranscurrido.");
}
// 5. Manejador del evento
private void ManejarSegundoTranscurrido(object sender, EventArgs e)
{
Console.WriteLine($"Pantalla: ¡TIC! {DateTime.Now.ToLongTimeString()}");
}
}
public class Registrador
{
public void Suscribirse(Reloj reloj)
{
reloj.SegundoTranscurrido += ManejarSegundoTranscurrido;
Console.WriteLine("Registrador: Suscrito al evento SegundoTranscurrido.");
}
public void Desuscribirse(Reloj reloj)
{
reloj.SegundoTranscurrido -= ManejarSegundoTranscurrido;
Console.WriteLine("Registrador: Desuscrito del evento SegundoTranscurrido.");
}
// 5. Manejador del evento
private void ManejarSegundoTranscurrido(object sender, EventArgs e)
{
Console.WriteLine($"Registrador: Guardando evento en log falso: {DateTime.Now.ToLongTimeString()}");
}
}
4. Programa Principal
using System;
public class ProgramaEventos
{
public static void Main(string[] args)
{
Reloj miReloj = new Reloj();
Pantalla miPantalla = new Pantalla();
Registrador miRegistrador = new Registrador();
// Suscribir los suscriptores al evento del reloj
miPantalla.Suscribirse(miReloj);
miRegistrador.Suscribirse(miReloj);
// Iniciar el reloj, que disparará los eventos
miReloj.Iniciar();
// Después de detener el reloj, podemos desuscribir un suscriptor
Console.WriteLine("\n--- Desuscribiendo Pantalla ---");
miPantalla.Desuscribirse(miReloj);
// Si volvemos a iniciar el reloj, Pantalla ya no recibirá notificaciones
// Esto es solo para demostración, en un caso real el reloj ya estaría apagado.
// Para ver el efecto, deberíamos reiniciar el reloj en un bucle.
// Por simplicidad, omitimos el reinicio aquí. Puedes probarlo modificando el bucle del Reloj.
Console.WriteLine("\nPrograma finalizado.");
}
}
Ejecución Esperada
Cuando ejecutes este código, verás que cada segundo, tanto la Pantalla como el Registrador reciben la notificación del Reloj y ejecutan sus respectivos métodos. Después de desuscribir la Pantalla, solo el Registrador continuará recibiendo las notificaciones (si el reloj siguiera activo).
¿Por qué `protected virtual void OnSegundoTranscurrido()`?
Esta es una convención de diseño de .NET.
protected: Permite que las clases derivadas (si las hubiera) sobrescriban este método para añadir lógica o cambiar el comportamiento del disparo del evento.virtual: Permite la sobrescritura en clases derivadas.void: El método no devuelve un valor.On[NombreEvento]: Es el nombre estándar para el método que eleva un evento. Esto hace que el código sea más legible y predecible para otros desarrolladores.
🔄 Variaciones y Patrones Comunes
Eventos con Expresiones Lambda
Las expresiones lambda son increíblemente útiles para suscribirse a eventos, especialmente para manejadores simples o inline.
using System;
public class ClickBoton
{
public event EventHandler Clicked;
public void SimularClick()
{
Console.WriteLine("Simulando click...");
Clicked?.Invoke(this, EventArgs.Empty);
}
}
public class ProgramaLambda
{
public static void Main(string[] args)
{
ClickBoton boton = new ClickBoton();
// Suscripción con expresión lambda
boton.Clicked += (sender, e) =>
{
Console.WriteLine("¡Botón clickeado con lambda!");
Console.WriteLine($"Sender: {((ClickBoton)sender).GetType().Name}");
};
boton.SimularClick();
}
}
Delegados como Parámetros de Métodos
Una de las aplicaciones más directas de los delegados es pasarlos como parámetros a otros métodos, lo que permite la inyección de comportamiento.
using System;
public class ProcesadorDatos
{
// Método que acepta un delegado como parámetro
public void ProcesarColeccion(int[] datos, Func<int, bool> filtro, Action<int> accion)
{
Console.WriteLine("\nProcesando colección...");
foreach (var item in datos)
{
if (filtro(item))
{
accion(item);
}
}
}
}
public class ProgramaParametrosDelegados
{
public static void Main(string[] args)
{
ProcesadorDatos procesador = new ProcesadorDatos();
int[] numeros = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Usando lambdas para el filtro y la acción
procesador.ProcesarColeccion(
numeros,
num => num % 2 == 0, // Filtro: Solo números pares
num => Console.WriteLine($"Encontrado par: {num}") // Acción: Imprimir el número par
);
procesador.ProcesarColeccion(
numeros,
num => num > 5, // Filtro: Solo números mayores que 5
num => Console.WriteLine($"Encontrado > 5: {num * 2}") // Acción: Imprimir el doble del número
);
}
}
⚠️ Consideraciones y Mejores Prácticas
Gestión de Suscripciones (Memory Leaks)
Una de las consideraciones más críticas al trabajar con eventos es la gestión adecuada de las suscripciones. Si un suscriptor se suscribe a un evento y nunca se desuscribe, el publicador mantendrá una referencia fuerte al suscriptor. Esto significa que el suscriptor no podrá ser recolectado por el Garbage Collector (GC), incluso si ya no está en uso, resultando en un memory leak.
- Siempre desuscribe: Si un suscriptor tiene un ciclo de vida limitado (ej. un formulario, un componente UI), asegúrate de desuscribirlo del evento del publicador cuando ya no lo necesite (ej. en el evento
DisposeoOnClosed). - Eventos estáticos: Ten especial cuidado con los eventos estáticos, ya que su vida útil es la de la aplicación. Si un objeto se suscribe a un evento estático y no se desuscribe, permanecerá en memoria permanentemente.
Manejo de Excepciones en Manejadores de Eventos
Si un manejador de eventos lanza una excepción, esta puede tener un comportamiento inesperado, especialmente con eventos multicast. Por defecto, la excepción detendrá la ejecución de los subsiguientes manejadores de eventos y se propagará hacia arriba. Para evitar que un manejador de eventos problemático afecte a otros:
- Encapsula con
try-catch: Dentro de los manejadores de eventos, es una buena práctica encapsular la lógica en bloquestry-catchpara gestionar excepciones localmente y evitar que se propaguen.
private void ManejarEventoConCuidado(object sender, EventArgs e)
{
try
{
// Lógica del manejador de eventos
// ...
}
catch (Exception ex)
{
// Loguear la excepción y/o manejarla de forma segura
Console.Error.WriteLine($"Error en manejador de evento: {ex.Message}");
}
}
¿Cuándo usar delegados simples vs. eventos?
| Característica | Delegado Simple (Action/Func) | Evento (event palabra clave) |
|---|---|---|
| Propósito | Pasador de comportamiento, callback | Mecanismo de notificación |
| Uso común | Lógica flexible, inyección de función | Desacoplamiento Publicador/Suscriptor |
| Multicast | Sí | Sí |
| Fuera de la clase | Puede ser invocado directamente | Solo el publicador puede invocarlo |
| Suscripción | Asignación =/+=/-= | +=/-= |
| Encapsulación | Baja | Alta (protege la invocación) |
| Riesgo ML | Si es campo/propiedad estática | Sí, si no se desuscribe |
🏁 Conclusión
Los delegados y eventos son herramientas muy poderosas en C# que te permiten diseñar arquitecturas de software más flexibles, desacopladas y reactivas. Comprender su funcionamiento y aplicarlos correctamente es crucial para construir aplicaciones mantenibles y escalables.
Desde la invocación de métodos como parámetros (Action y Func) hasta la creación de sistemas de notificación robustos, su dominio te abrirá las puertas a patrones de diseño avanzados y una mayor eficiencia en tu código. ¡Ahora estás listo para aplicar estos conceptos y mejorar significativamente la modularidad de tus proyectos C#! 🚀
Tutoriales relacionados
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!