Delegados y eventos
Los delegados y eventos representan uno de los conceptos más poderosos y elegantes de C#, proporcionando la base para la programación orientada a eventos y los patrones de diseño avanzados. Un delegado es esencialmente un puntero a función que permite tratar los métodos como objetos de primera clase, mientras que los eventos proporcionan una forma segura y estructurada de implementar el patrón observador. Estos conceptos son fundamentales para entender tecnologías como Windows Forms, WPF, ASP.NET y la programación asíncrona moderna.
La comprensión profunda de delegados y eventos no solo mejora tu capacidad para escribir código más flexible y desacoplado, sino que también te prepara para conceptos avanzados como las expresiones lambda, LINQ y la programación funcional. Estas herramientas permiten crear arquitecturas de software más modulares y mantenibles.
En este artículo exploraremos desde los fundamentos básicos de los delegados hasta implementaciones avanzadas de eventos, incluyendo patrones de diseño comunes y mejores prácticas para su uso en aplicaciones del mundo real.
Fundamentos de los delegados
Definición y conceptos básicos
Un delegado en C# es un tipo que define la signatura de un método y puede contener referencias a métodos estáticos o de instancia. Funciona como un puntero a función type-safe que puede invocar métodos de manera indirecta.
Característica | Descripción | Ventaja |
---|---|---|
Type-safe | Garantiza que los métodos tengan la signatura correcta | Seguridad en tiempo de compilación |
Multicast | Puede contener referencias a múltiples métodos | Invocación de múltiples métodos con una llamada |
Inmutable | Crear un nuevo delegado cuando se añaden/quitan métodos | Seguridad en hilos |
Nullable | Puede ser null si no referencia ningún método | Verificación de estado |
Declaración y uso básico
using System;
// Declaración de un tipo de delegado
public delegate void AccionSimple(string mensaje);
public delegate int OperacionMatematica(int a, int b);
public delegate bool PredicadoEntero(int numero);
class EjemplosDelegadosBasicos
{
static void Main()
{
// Ejemplo 1: Delegado de acción simple
AccionSimple accion = MostrarMensaje;
accion("Hola desde un delegado"); // Invocación
// Ejemplo 2: Delegado de operación matemática
OperacionMatematica suma = Sumar;
OperacionMatematica multiplicacion = Multiplicar;
int resultadoSuma = suma(5, 3);
int resultadoMultiplicacion = multiplicacion(5, 3);
Console.WriteLine($"Suma: {resultadoSuma}");
Console.WriteLine($"Multiplicación: {resultadoMultiplicacion}");
// Ejemplo 3: Delegado con predicado
PredicadoEntero esPar = EsNumeroPar;
PredicadoEntero esPositivo = EsNumeroPositivo;
Console.WriteLine($"¿4 es par? {esPar(4)}");
Console.WriteLine($"¿-5 es positivo? {esPositivo(-5)}");
}
static void MostrarMensaje(string mensaje)
{
Console.WriteLine($"Mensaje: {mensaje}");
}
static int Sumar(int a, int b)
{
return a + b;
}
static int Multiplicar(int a, int b)
{
return a * b;
}
static bool EsNumeroPar(int numero)
{
return numero % 2 == 0;
}
static bool EsNumeroPositivo(int numero)
{
return numero > 0;
}
}
Delegados predefinidos: Action, Func y Predicate
using System;
using System.Collections.Generic;
using System.Linq;
class DelegadosPredefinidos
{
static void Main()
{
// Action: delegado que no devuelve valor
Action<string> mostrar = mensaje => Console.WriteLine(mensaje);
Action<int, int> mostrarSuma = (a, b) => Console.WriteLine($"{a} + {b} = {a + b}");
mostrar("Usando Action");
mostrarSuma(10, 20);
// Func: delegado que devuelve un valor
Func<int, int, int> sumar = (a, b) => a + b;
Func<string, int> obtenerLongitud = texto => texto.Length;
Func<int> obtenerNumeroAleatorio = () => new Random().Next(1, 100);
Console.WriteLine($"Suma con Func: {sumar(15, 25)}");
Console.WriteLine($"Longitud de 'Hola Mundo': {obtenerLongitud("Hola Mundo")}");
Console.WriteLine($"Número aleatorio: {obtenerNumeroAleatorio()}");
// Predicate: delegado que devuelve bool
Predicate<int> esMayorQuecinco = numero => numero > 5;
Predicate<string> esTextoVacio = texto => string.IsNullOrEmpty(texto);
Console.WriteLine($"¿8 es mayor que 5? {esMayorQuecinco(8)}");
Console.WriteLine($"¿'' está vacío? {esTextoVacio("")}");
// Ejemplo práctico: filtrar una lista
List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var numerosPares = numeros.Where(n => n % 2 == 0).ToList();
var numerosGrandes = numeros.Where(n => n > 5).ToList();
Console.WriteLine($"Números pares: {string.Join(", ", numerosPares)}");
Console.WriteLine($"Números > 5: {string.Join(", ", numerosGrandes)}");
}
}
Delegados multicast
Combinación y invocación múltiple
Los delegados multicast permiten combinar múltiples métodos en un solo delegado, ejecutándolos secuencialmente:
using System;
class EjemploMulticast
{
static void Main()
{
// Delegado multicast de acción
Action<string> notificadores = null;
// Añadir métodos al delegado
notificadores += NotificarConsola;
notificadores += NotificarArchivo;
notificadores += NotificarEmail;
// Invocar todos los métodos de una vez
Console.WriteLine("=== Enviando notificación ===");
notificadores?.Invoke("Sistema iniciado correctamente");
Console.WriteLine("\n=== Quitando notificador de email ===");
notificadores -= NotificarEmail;
notificadores?.Invoke("Operación completada");
// Ejemplo con delegado que devuelve valor
Func<int, int> operaciones = null;
operaciones += DuplicarNumero;
operaciones += TriplicarNumero;
operaciones += CuadrarNumero;
// IMPORTANTE: En delegados multicast que devuelven valor,
// solo se obtiene el resultado del último método
Console.WriteLine($"\nResultado final: {operaciones(5)}"); // Solo el resultado de CuadrarNumero
// Para obtener todos los resultados, usar GetInvocationList
Console.WriteLine("\n=== Resultados individuales ===");
foreach (Func<int, int> operacion in operaciones.GetInvocationList())
{
int resultado = operacion(5);
Console.WriteLine($"Resultado: {resultado}");
}
}
static void NotificarConsola(string mensaje)
{
Console.WriteLine($"[CONSOLA] {DateTime.Now:HH:mm:ss} - {mensaje}");
}
static void NotificarArchivo(string mensaje)
{
Console.WriteLine($"[ARCHIVO] Guardando en log.txt: {mensaje}");
// Aquí iría la lógica real de escritura a archivo
}
static void NotificarEmail(string mensaje)
{
Console.WriteLine($"[EMAIL] Enviando a admin@empresa.com: {mensaje}");
// Aquí iría la lógica real de envío de email
}
static int DuplicarNumero(int numero)
{
int resultado = numero * 2;
Console.WriteLine($"Duplicando {numero} = {resultado}");
return resultado;
}
static int TriplicarNumero(int numero)
{
int resultado = numero * 3;
Console.WriteLine($"Triplicando {numero} = {resultado}");
return resultado;
}
static int CuadrarNumero(int numero)
{
int resultado = numero * numero;
Console.WriteLine($"Cuadrando {numero} = {resultado}");
return resultado;
}
}
Sistema de logging con delegados multicast
using System;
using System.IO;
using System.Collections.Generic;
public enum NivelLog
{
Debug,
Info,
Warning,
Error
}
public class GestorLogging
{
private Dictionary<NivelLog, Action<string>> loggers;
public GestorLogging()
{
loggers = new Dictionary<NivelLog, Action<string>>();
// Configurar diferentes niveles de logging
loggers[NivelLog.Debug] = LogConsola;
loggers[NivelLog.Info] = LogConsola;
loggers[NivelLog.Warning] = LogConsola;
loggers[NivelLog.Warning] += LogArchivo; // Warning también va a archivo
loggers[NivelLog.Error] = LogConsola;
loggers[NivelLog.Error] += LogArchivo;
loggers[NivelLog.Error] += LogEmail; // Error va a todos lados
}
public void Log(NivelLog nivel, string mensaje)
{
string mensajeCompleto = $"[{nivel}] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {mensaje}";
if (loggers.ContainsKey(nivel))
{
loggers[nivel]?.Invoke(mensajeCompleto);
}
}
public void AgregarLogger(NivelLog nivel, Action<string> logger)
{
if (loggers.ContainsKey(nivel))
{
loggers[nivel] += logger;
}
else
{
loggers[nivel] = logger;
}
}
public void QuitarLogger(NivelLog nivel, Action<string> logger)
{
if (loggers.ContainsKey(nivel))
{
loggers[nivel] -= logger;
}
}
private void LogConsola(string mensaje)
{
Console.WriteLine(mensaje);
}
private void LogArchivo(string mensaje)
{
// Simular escritura a archivo
Console.WriteLine($" >> Escribiendo a archivo: {mensaje}");
}
private void LogEmail(string mensaje)
{
// Simular envío de email
Console.WriteLine($" >> Enviando email de alerta: {mensaje}");
}
}
class EjemploSistemaLogging
{
static void Main()
{
var logger = new GestorLogging();
// Probar diferentes niveles
logger.Log(NivelLog.Debug, "Iniciando aplicación");
logger.Log(NivelLog.Info, "Usuario conectado: juan@empresa.com");
logger.Log(NivelLog.Warning, "Memoria baja detectada");
logger.Log(NivelLog.Error, "Error de conexión a base de datos");
// Agregar logger personalizado
Action<string> logPersonalizado = mensaje =>
Console.WriteLine($" >> LOG PERSONALIZADO: {mensaje}");
logger.AgregarLogger(NivelLog.Info, logPersonalizado);
Console.WriteLine("\n=== Con logger personalizado ===");
logger.Log(NivelLog.Info, "Nuevo mensaje con logger personalizado");
}
}
Introducción a los eventos
Diferencia entre delegados y eventos
Los eventos son una forma especial de delegados que proporcionan encapsulación y control de acceso:
Aspecto | Delegado | Evento |
---|---|---|
Acceso | Público: cualquiera puede invocar | Protegido: solo la clase propietaria puede disparar |
Asignación | Permite asignación directa (=) | Solo permite suscripción (+= y -=) |
Invocación | Cualquier código puede invocar | Solo el código de la clase propietaria |
Encapsulación | Menor encapsulación | Mayor encapsulación y seguridad |
Declaración y uso básico de eventos
using System;
// Definir argumentos personalizados para el evento
public class TemperaturaEventArgs : EventArgs
{
public double Temperatura { get; }
public DateTime FechaMedicion { get; }
public bool EsAlarmante { get; }
public TemperaturaEventArgs(double temperatura)
{
Temperatura = temperatura;
FechaMedicion = DateTime.Now;
EsAlarmante = temperatura > 30 || temperatura < 0;
}
}
// Clase que publica eventos
public class SensorTemperatura
{
// Declaración del evento
public event EventHandler<TemperaturaEventArgs> TemperaturaCambiada;
public event EventHandler<TemperaturaEventArgs> TemperaturaAlarmante;
private double temperatura;
public double Temperatura
{
get => temperatura;
set
{
if (Math.Abs(temperatura - value) > 0.1) // Cambio significativo
{
temperatura = value;
OnTemperaturaCambiada(new TemperaturaEventArgs(temperatura));
if (temperatura > 30 || temperatura < 0)
{
OnTemperaturaAlarmante(new TemperaturaEventArgs(temperatura));
}
}
}
}
// Métodos protegidos para disparar eventos
protected virtual void OnTemperaturaCambiada(TemperaturaEventArgs e)
{
TemperaturaCambiada?.Invoke(this, e);
}
protected virtual void OnTemperaturaAlarmante(TemperaturaEventArgs e)
{
TemperaturaAlarmante?.Invoke(this, e);
}
public void SimularCambios()
{
Random random = new Random();
for (int i = 0; i < 10; i++)
{
Temperatura = random.Next(-5, 40);
System.Threading.Thread.Sleep(500);
}
}
}
// Clases que se suscriben a eventos
public class MonitorTemperatura
{
public string Nombre { get; }
public MonitorTemperatura(string nombre)
{
Nombre = nombre;
}
public void SuscribirseA(SensorTemperatura sensor)
{
sensor.TemperaturaCambiada += ManejardorTemperaturaCambiada;
sensor.TemperaturaAlarmante += ManejadorTemperaturaAlarmante;
}
public void DesuscribirseDe(SensorTemperatura sensor)
{
sensor.TemperaturaCambiada -= ManejardorTemperaturaCambiada;
sensor.TemperaturaAlarmante -= ManejadorTemperaturaAlarmante;
}
private void ManejardorTemperaturaCambiada(object sender, TemperaturaEventArgs e)
{
Console.WriteLine($"[{Nombre}] Temperatura registrada: {e.Temperatura:F1}°C a las {e.FechaMedicion:HH:mm:ss}");
}
private void ManejadorTemperaturaAlarmante(object sender, TemperaturaEventArgs e)
{
Console.WriteLine($"[{Nombre}] ¡ALERTA! Temperatura alarmante: {e.Temperatura:F1}°C");
}
}
class EjemploEventosBasicos
{
static void Main()
{
var sensor = new SensorTemperatura();
var monitor1 = new MonitorTemperatura("Monitor Central");
var monitor2 = new MonitorTemperatura("Monitor Respaldo");
// Suscribir monitores al sensor
monitor1.SuscribirseA(sensor);
monitor2.SuscribirseA(sensor);
Console.WriteLine("=== Iniciando simulación de temperatura ===");
sensor.SimularCambios();
// Desuscribir un monitor
Console.WriteLine("\n=== Desuscribiendo Monitor Respaldo ===");
monitor2.DesuscribirseDe(sensor);
sensor.Temperatura = 35; // Solo Monitor Central recibirá esta notificación
sensor.Temperatura = -2; // Solo Monitor Central recibirá esta notificación
}
}
Eventos avanzados y patrones comunes
Patrón Observer con eventos
using System;
using System.Collections.Generic;
// Interfaz para el patrón Observer
public interface IObservador<T>
{
void Actualizar(T dato);
}
// Clase Observable genérica
public class Observable<T>
{
public event Action<T> DatoCambiado;
private T valor;
public T Valor
{
get => valor;
set
{
if (!EqualityComparer<T>.Default.Equals(valor, value))
{
valor = value;
NotificarCambio(valor);
}
}
}
protected virtual void NotificarCambio(T nuevoValor)
{
DatoCambiado?.Invoke(nuevoValor);
}
}
// Implementación de observadores
public class EstadisticasObservador : IObservador<int>
{
private int suma = 0;
private int contador = 0;
public double Promedio => contador > 0 ? (double)suma / contador : 0;
public void Actualizar(int valor)
{
suma += valor;
contador++;
Console.WriteLine($"[Estadísticas] Nuevo valor: {valor}, Promedio actual: {Promedio:F2}");
}
}
public class HistorialObservador<T> : IObservador<T>
{
private readonly Queue<(DateTime, T)> historial = new Queue<(DateTime, T)>();
private readonly int tamaioMaximo;
public HistorialObservador(int tamaioMaximo = 10)
{
this.tamaioMaximo = tamaioMaximo;
}
public void Actualizar(T valor)
{
historial.Enqueue((DateTime.Now, valor));
while (historial.Count > tamaioMaximo)
{
historial.Dequeue();
}
Console.WriteLine($"[Historial] Guardado: {valor} (Total: {historial.Count} registros)");
}
public void MostrarHistorial()
{
Console.WriteLine("=== Historial completo ===");
foreach (var (fecha, valor) in historial)
{
Console.WriteLine($" {fecha:HH:mm:ss} - {valor}");
}
}
}
// Sistema de monitoreo de stock
public class GestorStock
{
public event EventHandler<StockEventArgs> StockBajo;
public event EventHandler<StockEventArgs> StockAgotado;
public event EventHandler<StockEventArgs> StockRepuesto;
private readonly Dictionary<string, int> inventario = new Dictionary<string, int>();
private readonly Dictionary<string, int> stockMinimo = new Dictionary<string, int>();
public void EstablecerStockMinimo(string producto, int minimo)
{
stockMinimo[producto] = minimo;
}
public void ActualizarStock(string producto, int cantidad)
{
int stockAnterior = inventario.ContainsKey(producto) ? inventario[producto] : 0;
inventario[producto] = cantidad;
var args = new StockEventArgs(producto, stockAnterior, cantidad);
// Verificar condiciones de stock
if (cantidad == 0 && stockAnterior > 0)
{
OnStockAgotado(args);
}
else if (cantidad > 0 && stockAnterior == 0)
{
OnStockRepuesto(args);
}
else if (stockMinimo.ContainsKey(producto) && cantidad <= stockMinimo[producto] && stockAnterior > stockMinimo[producto])
{
OnStockBajo(args);
}
}
protected virtual void OnStockBajo(StockEventArgs e)
{
StockBajo?.Invoke(this, e);
}
protected virtual void OnStockAgotado(StockEventArgs e)
{
StockAgotado?.Invoke(this, e);
}
protected virtual void OnStockRepuesto(StockEventArgs e)
{
StockRepuesto?.Invoke(this, e);
}
}
public class StockEventArgs : EventArgs
{
public string Producto { get; }
public int StockAnterior { get; }
public int StockActual { get; }
public DateTime Timestamp { get; }
public StockEventArgs(string producto, int stockAnterior, int stockActual)
{
Producto = producto;
StockAnterior = stockAnterior;
StockActual = stockActual;
Timestamp = DateTime.Now;
}
}
class EjemploPatronObserver
{
static void Main()
{
// Ejemplo con Observable genérico
var numeroObservable = new Observable<int>();
var estadisticas = new EstadisticasObservador();
var historial = new HistorialObservador<int>();
numeroObservable.DatoCambiado += estadisticas.Actualizar;
numeroObservable.DatoCambiado += historial.Actualizar;
Console.WriteLine("=== Ejemplo Observable genérico ===");
Random random = new Random();
for (int i = 0; i < 5; i++)
{
numeroObservable.Valor = random.Next(1, 100);
}
historial.MostrarHistorial();
// Ejemplo con gestor de stock
Console.WriteLine("\n=== Ejemplo Gestor de Stock ===");
var gestorStock = new GestorStock();
// Suscribirse a eventos
gestorStock.StockBajo += (sender, e) =>
Console.WriteLine($"⚠️ STOCK BAJO: {e.Producto} ({e.StockActual} unidades)");
gestorStock.StockAgotado += (sender, e) =>
Console.WriteLine($"🚫 STOCK AGOTADO: {e.Producto}");
gestorStock.StockRepuesto += (sender, e) =>
Console.WriteLine($"✅ STOCK REPUESTO: {e.Producto} ({e.StockActual} unidades)");
// Configurar y simular cambios de stock
gestorStock.EstablecerStockMinimo("Laptop", 5);
gestorStock.EstablecerStockMinimo("Mouse", 10);
gestorStock.ActualizarStock("Laptop", 20);
gestorStock.ActualizarStock("Laptop", 3); // Stock bajo
gestorStock.ActualizarStock("Laptop", 0); // Stock agotado
gestorStock.ActualizarStock("Laptop", 15); // Stock repuesto
gestorStock.ActualizarStock("Mouse", 8); // Stock bajo
}
}
Sistema de notificaciones con prioridades
using System;
using System.Collections.Generic;
using System.Linq;
public enum PrioridadNotificacion
{
Baja = 1,
Normal = 2,
Alta = 3,
Critica = 4
}
public class NotificacionEventArgs : EventArgs
{
public string Mensaje { get; }
public PrioridadNotificacion Prioridad { get; }
public DateTime Timestamp { get; }
public string Remitente { get; }
public NotificacionEventArgs(string mensaje, PrioridadNotificacion prioridad, string remitente)
{
Mensaje = mensaje;
Prioridad = prioridad;
Remitente = remitente;
Timestamp = DateTime.Now;
}
}
public class CentroNotificaciones
{
// Eventos por prioridad
public event EventHandler<NotificacionEventArgs> NotificacionRecibida;
public event EventHandler<NotificacionEventArgs> NotificacionCritica;
public event EventHandler<NotificacionEventArgs> NotificacionAlta;
private readonly List<NotificacionEventArgs> historialNotificaciones = new List<NotificacionEventArgs>();
public void EnviarNotificacion(string mensaje, PrioridadNotificacion prioridad, string remitente)
{
var notificacion = new NotificacionEventArgs(mensaje, prioridad, remitente);
historialNotificaciones.Add(notificacion);
// Disparar evento general
OnNotificacionRecibida(notificacion);
// Disparar eventos específicos por prioridad
switch (prioridad)
{
case PrioridadNotificacion.Critica:
OnNotificacionCritica(notificacion);
break;
case PrioridadNotificacion.Alta:
OnNotificacionAlta(notificacion);
break;
}
}
public IEnumerable<NotificacionEventArgs> ObtenerNotificacionesPor(PrioridadNotificacion prioridad)
{
return historialNotificaciones.Where(n => n.Prioridad == prioridad);
}
public void LimpiarHistorial(PrioridadNotificacion? prioridadMinima = null)
{
if (prioridadMinima.HasValue)
{
historialNotificaciones.RemoveAll(n => n.Prioridad < prioridadMinima.Value);
}
else
{
historialNotificaciones.Clear();
}
}
protected virtual void OnNotificacionRecibida(NotificacionEventArgs e)
{
NotificacionRecibida?.Invoke(this, e);
}
protected virtual void OnNotificacionCritica(NotificacionEventArgs e)
{
NotificacionCritica?.Invoke(this, e);
}
protected virtual void OnNotificacionAlta(NotificacionEventArgs e)
{
NotificacionAlta?.Invoke(this, e);
}
}
// Manejadores especializados
public class ManejadorConsola
{
public void ManejarNotificacion(object sender, NotificacionEventArgs e)
{
string icono = e.Prioridad switch
{
PrioridadNotificacion.Baja => "ℹ️",
PrioridadNotificacion.Normal => "📋",
PrioridadNotificacion.Alta => "⚠️",
PrioridadNotificacion.Critica => "🚨",
_ => "📌"
};
Console.WriteLine($"{icono} [{e.Prioridad}] {e.Remitente}: {e.Mensaje} ({e.Timestamp:HH:mm:ss})");
}
}
public class ManejadorEmergencia
{
public void ManejarNotificacionCritica(object sender, NotificacionEventArgs e)
{
Console.WriteLine("🚨🚨🚨 ALERTA CRÍTICA 🚨🚨🚨");
Console.WriteLine($"Mensaje: {e.Mensaje}");
Console.WriteLine($"Remitente: {e.Remitente}");
Console.WriteLine($"Hora: {e.Timestamp:HH:mm:ss}");
Console.WriteLine("Activando protocolo de emergencia...");
Console.WriteLine("🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨");
}
public void ManejarNotificacionAlta(object sender, NotificacionEventArgs e)
{
Console.WriteLine($"⚠️ ATENCIÓN REQUERIDA: {e.Mensaje}");
Console.WriteLine($" Escalando a supervisor - Remitente: {e.Remitente}");
}
}
class EjemploSistemaNotificaciones
{
static void Main()
{
var centroNotificaciones = new CentroNotificaciones();
var manejadorConsola = new ManejadorConsola();
var manejadorEmergencia = new ManejadorEmergencia();
// Suscribir manejadores
centroNotificaciones.NotificacionRecibida += manejadorConsola.ManejarNotificacion;
centroNotificaciones.NotificacionCritica += manejadorEmergencia.ManejarNotificacionCritica;
centroNotificaciones.NotificacionAlta += manejadorEmergencia.ManejarNotificacionAlta;
// Simular diferentes tipos de notificaciones
Console.WriteLine("=== Sistema de Notificaciones Iniciado ===\n");
centroNotificaciones.EnviarNotificacion(
"Sistema iniciado correctamente",
PrioridadNotificacion.Normal,
"Sistema"
);
centroNotificaciones.EnviarNotificacion(
"Memoria RAM al 85% de uso",
PrioridadNotificacion.Alta,
"Monitor Sistema"
);
centroNotificaciones.EnviarNotificacion(
"Nuevo usuario registrado",
PrioridadNotificacion.Baja,
"Módulo Usuarios"
);
centroNotificaciones.EnviarNotificacion(
"¡Fallo crítico en base de datos principal!",
PrioridadNotificacion.Critica,
"DB Monitor"
);
centroNotificaciones.EnviarNotificacion(
"Backup completado exitosamente",
PrioridadNotificacion.Normal,
"Sistema Backup"
);
// Mostrar estadísticas
Console.WriteLine("\n=== Estadísticas de Notificaciones ===");
foreach (PrioridadNotificacion prioridad in Enum.GetValues<PrioridadNotificacion>())
{
var count = centroNotificaciones.ObtenerNotificacionesPor(prioridad).Count();
if (count > 0)
{
Console.WriteLine($"{prioridad}: {count} notificaciones");
}
}
}
}
Delegados con métodos anónimos y expresiones lambda
Evolución de la sintaxis
using System;
using System.Collections.Generic;
using System.Linq;
class EvolucionSintaxis
{
static void Main()
{
List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 1. Método tradicional con delegado
Predicate<int> esPar1 = new Predicate<int>(EsNumeroPar);
var paresTradicionais = numeros.Where(n => esPar1(n)).ToList();
// 2. Método anónimo (C# 2.0)
Predicate<int> esPar2 = delegate(int numero) { return numero % 2 == 0; };
var paresAnonimos = numeros.Where(n => esPar2(n)).ToList();
// 3. Expresión lambda (C# 3.0)
Predicate<int> esPar3 = numero => numero % 2 == 0;
var paresLambda = numeros.Where(n => esPar3(n)).ToList();
// 4. Lambda inline más concisa
var paresConciso = numeros.Where(n => n % 2 == 0).ToList();
Console.WriteLine($"Pares encontrados: {string.Join(", ", paresConciso)}");
// Ejemplos más complejos con lambdas
EjemplosLambdasComplejas(numeros);
}
static bool EsNumeroPar(int numero)
{
return numero % 2 == 0;
}
static void EjemplosLambdasComplejas(List<int> numeros)
{
Console.WriteLine("\n=== Ejemplos con Lambdas Complejas ===");
// Lambda con múltiples parámetros
Func<int, int, string> compararNumeros = (a, b) =>
{
if (a > b) return $"{a} es mayor que {b}";
if (a < b) return $"{a} es menor que {b}";
return $"{a} es igual a {b}";
};
Console.WriteLine(compararNumeros(5, 3));
Console.WriteLine(compararNumeros(2, 8));
// Lambda con captura de variables locales (closure)
int multiplicador = 3;
Func<int, int> multiplicarPor = x => x * multiplicador;
var numerosMultiplicados = numeros.Select(multiplicarPor).ToList();
Console.WriteLine($"Multiplicados por {multiplicador}: {string.Join(", ", numerosMultiplicados)}");
// Cambiar la variable capturada
multiplicador = 5;
var nuevosMultiplicados = numeros.Select(multiplicarPor).ToList();
Console.WriteLine($"Multiplicados por {multiplicador}: {string.Join(", ", nuevosMultiplicados)}");
// Lambda que devuelve otra lambda (función de orden superior)
Func<int, Func<int, int>> crearMultiplicador = factor => x => x * factor;
var multiplicarPor2 = crearMultiplicador(2);
var multiplicarPor10 = crearMultiplicador(10);
Console.WriteLine($"5 * 2 = {multiplicarPor2(5)}");
Console.WriteLine($"5 * 10 = {multiplicarPor10(5)}");
}
}
Uso avanzado con eventos y lambdas
using System;
using System.Collections.Generic;
using System.Timers;
public class ProcessorEventArgs : EventArgs
{
public string TareaId { get; }
public string Estado { get; }
public double Progreso { get; }
public TimeSpan TiempoTranscurrido { get; }
public ProcessorEventArgs(string tareaId, string estado, double progreso, TimeSpan tiempoTranscurrido)
{
TareaId = tareaId;
Estado = estado;
Progreso = progreso;
TiempoTranscurrido = tiempoTranscurrido;
}
}
public class ProcesadorTareas
{
public event EventHandler<ProcessorEventArgs> ProgresoActualizado;
public event EventHandler<ProcessorEventArgs> TareaCompletada;
public event EventHandler<ProcessorEventArgs> TareaFalló;
private readonly Dictionary<string, DateTime> tareasIniciadas = new Dictionary<string, DateTime>();
private readonly Timer timer = new Timer(500); // Actualizar cada 500ms
public ProcesadorTareas()
{
timer.Elapsed += (sender, e) => ActualizarProgreso();
}
public void IniciarTarea(string tareaId, Action<Action<double>> tarea)
{
tareasIniciadas[tareaId] = DateTime.Now;
timer.Start();
// Ejecutar tarea en un hilo separado
System.Threading.Tasks.Task.Run(() =>
{
try
{
// Callback para reportar progreso
Action<double> reportarProgreso = progreso =>
{
var tiempoTranscurrido = DateTime.Now - tareasIniciadas[tareaId];
OnProgresoActualizado(new ProcessorEventArgs(tareaId, "En proceso", progreso, tiempoTranscurrido));
};
tarea(reportarProgreso);
// Tarea completada
var tiempoFinal = DateTime.Now - tareasIniciadas[tareaId];
OnTareaCompletada(new ProcessorEventArgs(tareaId, "Completada", 100, tiempoFinal));
}
catch (Exception ex)
{
var tiempoError = DateTime.Now - tareasIniciadas[tareaId];
OnTareaFalló(new ProcessorEventArgs(tareaId, $"Error: {ex.Message}", 0, tiempoError));
}
finally
{
tareasIniciadas.Remove(tareaId);
if (tareasIniciadas.Count == 0)
{
timer.Stop();
}
}
});
}
private void ActualizarProgreso()
{
// Actualización periódica para tareas de larga duración
foreach (var kvp in tareasIniciadas)
{
var tiempoTranscurrido = DateTime.Now - kvp.Value;
// Este evento se puede usar para heartbeat o monitoreo
}
}
protected virtual void OnProgresoActualizado(ProcessorEventArgs e)
{
ProgresoActualizado?.Invoke(this, e);
}
protected virtual void OnTareaCompletada(ProcessorEventArgs e)
{
TareaCompletada?.Invoke(this, e);
}
protected virtual void OnTareaFalló(ProcessorEventArgs e)
{
TareaFalló?.Invoke(this, e);
}
}
class EjemploLambdasYEventos
{
static void Main()
{
var procesador = new ProcesadorTareas();
// Suscribirse a eventos usando lambdas
procesador.ProgresoActualizado += (sender, e) =>
Console.WriteLine($"[{e.TareaId}] Progreso: {e.Progreso:F1}% - Tiempo: {e.TiempoTranscurrido.TotalSeconds:F1}s");
procesador.TareaCompletada += (sender, e) =>
{
Console.WriteLine($"✅ [{e.TareaId}] COMPLETADA en {e.TiempoTranscurrido.TotalSeconds:F1}s");
Console.WriteLine(" Enviando notificación de finalización...");
};
procesador.TareaFalló += (sender, e) =>
{
Console.WriteLine($"❌ [{e.TareaId}] FALLÓ: {e.Estado}");
Console.WriteLine(" Iniciando procedimiento de recuperación...");
};
// Crear diferentes tipos de tareas usando lambdas
Console.WriteLine("=== Iniciando procesamiento de tareas ===\n");
// Tarea 1: Simulación de descarga
procesador.IniciarTarea("DESCARGA_001", reportarProgreso =>
{
for (int i = 0; i <= 100; i += 10)
{
System.Threading.Thread.Sleep(200);
reportarProgreso(i);
}
});
// Tarea 2: Simulación de procesamiento que falla
procesador.IniciarTarea("PROCESO_002", reportarProgreso =>
{
for (int i = 0; i <= 50; i += 5)
{
System.Threading.Thread.Sleep(150);
reportarProgreso(i);
if (i >= 30) // Simular fallo a los 30%
{
throw new InvalidOperationException("Error simulado en el procesamiento");
}
}
});
// Tarea 3: Procesamiento exitoso más rápido
procesador.IniciarTarea("RAPIDA_003", reportarProgreso =>
{
var random = new Random();
for (int i = 0; i <= 100; i += random.Next(5, 20))
{
System.Threading.Thread.Sleep(100);
reportarProgreso(Math.Min(i, 100));
}
});
// Esperar a que terminen las tareas
Console.WriteLine("Presiona cualquier tecla para continuar...");
Console.ReadKey();
}
}
Mejores prácticas y consideraciones de rendimiento
Gestión de memoria y suscripciones
using System;
using System.Collections.Generic;
using System.Weak;
public class GestorRecursos : IDisposable
{
public event EventHandler<string> RecursoLiberado;
private readonly List<string> recursos = new List<string>();
private bool disposed = false;
public void AgregarRecurso(string recurso)
{
recursos.Add(recurso);
Console.WriteLine($"Recurso agregado: {recurso}");
}
public void LiberarRecurso(string recurso)
{
if (recursos.Remove(recurso))
{
OnRecursoLiberado(recurso);
Console.WriteLine($"Recurso liberado: {recurso}");
}
}
protected virtual void OnRecursoLiberado(string recurso)
{
RecursoLiberado?.Invoke(this, recurso);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Limpiar recursos manejados
foreach (var recurso in recursos.ToArray())
{
LiberarRecurso(recurso);
}
recursos.Clear();
// Importante: limpiar suscripciones de eventos
RecursoLiberado = null;
}
disposed = true;
}
}
}
// Clase que demuestra suscripción y desuscripción apropiada
public class MonitorRecursos : IDisposable
{
private readonly GestorRecursos gestor;
private bool disposed = false;
public MonitorRecursos(GestorRecursos gestor)
{
this.gestor = gestor;
// Suscribirse al evento
gestor.RecursoLiberado += ManejadorRecursoLiberado;
}
private void ManejadorRecursoLiberado(object sender, string recurso)
{
Console.WriteLine($"[Monitor] Detectada liberación de recurso: {recurso}");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// IMPORTANTE: Desuscribirse para evitar memory leaks
if (gestor != null)
{
gestor.RecursoLiberado -= ManejadorRecursoLiberado;
}
}
disposed = true;
}
}
}
// Implementación de weak events para evitar memory leaks
public static class WeakEventManager
{
private static readonly Dictionary<object, List<WeakReference>> subscriptions =
new Dictionary<object, List<WeakReference>>();
public static void Subscribe<T>(EventHandler<T> handler, object source, string eventName) where T : EventArgs
{
if (!subscriptions.ContainsKey(source))
{
subscriptions[source] = new List<WeakReference>();
}
subscriptions[source].Add(new WeakReference(handler.Target));
}
public static void Cleanup()
{
var keysToRemove = new List<object>();
foreach (var kvp in subscriptions)
{
var aliveReferences = new List<WeakReference>();
foreach (var weakRef in kvp.Value)
{
if (weakRef.IsAlive)
{
aliveReferences.Add(weakRef);
}
}
if (aliveReferences.Count == 0)
{
keysToRemove.Add(kvp.Key);
}
else
{
subscriptions[kvp.Key] = aliveReferences;
}
}
foreach (var key in keysToRemove)
{
subscriptions.Remove(key);
}
}
}
class EjemploMejoresPracticas
{
static void Main()
{
Console.WriteLine("=== Demostración de Mejores Prácticas ===\n");
// Usar 'using' para garantizar la limpieza
using (var gestor = new GestorRecursos())
{
using (var monitor = new MonitorRecursos(gestor))
{
gestor.AgregarRecurso("Archivo1.txt");
gestor.AgregarRecurso("Conexión BD");
gestor.AgregarRecurso("Socket Red");
gestor.LiberarRecurso("Archivo1.txt");
gestor.LiberarRecurso("Socket Red");
} // MonitorRecursos se desuscribe automáticamente aquí
} // GestorRecursos limpia todos los recursos aquí
Console.WriteLine("\n=== Limpieza automática completada ===");
// Forzar garbage collection para demostrar la limpieza
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Garbage collection ejecutado");
}
}
Resumen
Los delegados y eventos en C# proporcionan herramientas poderosas para crear aplicaciones flexibles y desacopladas. Los delegados actúan como punteros a función type-safe que permiten tratar métodos como objetos de primera clase, mientras que los eventos ofrecen una forma segura y estructurada de implementar el patrón observador.
El dominio de estos conceptos incluye comprender la diferencia entre delegados simples y multicast, la evolución desde métodos anónimos hasta expresiones lambda, y la implementación correcta de eventos con manejo apropiado de memoria. Las mejores prácticas incluyen siempre desuscribirse de eventos para evitar memory leaks, usar patrones como weak events cuando sea necesario, y implementar IDisposable correctamente en clases que manejan eventos. Estos fundamentos son esenciales para tecnologías avanzadas como LINQ, programación asíncrona y frameworks de interfaz de usuario en .NET.