Ir al contenido principal

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.