tutoriales.com

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.

Intermedio18 min de lectura4 views15 de marzo de 2026Reportar error

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 en Delegate o, 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.
💡 Consejo: La firma del delegado (tipo de retorno y parámetros) debe coincidir exactamente con la de los métodos que se le asignarán.

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.
    }
}
⚠️ Advertencia: Si un delegado multicast no devuelve `void`, el valor de retorno de la invocación será el del *último* método en la lista. Esto puede ser confuso y a menudo no es el comportamiento deseado. Por ello, los delegados para eventos 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, void de retorno.
  • Action<T>: Un parámetro de tipo T, void de retorno.
  • Action<T1, T2>: Dos parámetros de tipo T1 y T2, void de 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, devuelve TResult.
  • Func<T, TResult>: Un parámetro de tipo T, devuelve TResult.
  • Func<T1, T2, TResult>: Dos parámetros de tipo T1 y T2, devuelve TResult.

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")}");
    }
}
Delegados dominados al 60%

🔗 ¿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.
Eventos Publicador (Publisher) Suscriptor (Subscriber) Evento (keyword) Delegado

⚡ 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 tipo EventArgs que 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 personalizada TEventArgs con los datos del evento.
📌 Nota: Es una buena práctica crear una clase `EventArgs` personalizada para cada tipo de evento que necesite pasar datos, incluso si solo contiene una propiedad. Esto hace el código más claro y extensible.

Pasos para Implementar Eventos

  1. Definir EventArgs (si se necesitan datos): Crea una clase que herede de System.EventArgs para contener cualquier dato relevante para el evento.
  2. Declarar el Delegado: Generalmente, usas EventHandler o EventHandler<TEventArgs>.
  3. Declarar el Evento: En la clase publicadora, declara el evento usando la palabra clave event y el tipo de delegado.
  4. 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()).
  5. 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 +=.
  6. 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
        );
    }
}
🔥 Importante: Este patrón es la base de muchas funciones de orden superior y es clave en la programación funcional en C#.

⚠️ 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 Dispose o OnClosed).
  • 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 bloques try-catch para 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ísticaDelegado Simple (Action/Func)Evento (event palabra clave)
PropósitoPasador de comportamiento, callbackMecanismo de notificación
Uso comúnLógica flexible, inyección de funciónDesacoplamiento Publicador/Suscriptor
Multicast
Fuera de la clasePuede ser invocado directamenteSolo el publicador puede invocarlo
SuscripciónAsignación =/+=/-=+=/-=
EncapsulaciónBajaAlta (protege la invocación)
Riesgo MLSi es campo/propiedad estáticaSí, si no se desuscribe
💡 Consejo: Usa `event` cuando quieras establecer un contrato de notificación donde la clase publicadora no necesite saber qué suscriptores existen, y donde solo la clase publicadora pueda disparar el evento. Usa delegados simples (`Action`/`Func`) cuando solo necesites pasar un método como argumento o almacenar una única referencia a una función.

🏁 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!