Cargando...

Delegados y Eventos en C#

Aprende delegados y eventos en C# para crear aplicaciones basadas en eventos con callbacks y ejemplos prácticos.

En C#, los delegates y los events constituyen la base de la programación orientada a eventos. Un delegate es una referencia segura en tipos (type-safe) que puede apuntar a un método. Un event permite que una clase notifique al mundo exterior que “una determinada acción ha ocurrido”. Estos mecanismos permiten construir arquitecturas débilmente acopladas (loosely coupled) y hacen posible ejecutar distintos métodos de forma dinámica.


¿Qué es un Delegate?

Un delegate es un tipo que puede contener referencias a métodos con una firma específica (parámetros y tipo de retorno). En otras palabras, permite tratar los métodos como si fueran variables.

Una variable delegate puede apuntar a un método compatible e invocarlo dinámicamente. Esto permite escribir código flexible y débilmente acoplado.


// Definición de un delegate
public delegate void Notify(string message);

class Program
{
    static void SendEmail(string message)
    {
        Console.WriteLine("Email enviado: " + message);
    }

    static void Main()
    {
        Notify notifyHandler;

        // Asignar un método al delegate
        notifyHandler = SendEmail;

        notifyHandler("La reunión es a las 10:00 AM.");
    }
}

En este ejemplo, el delegate notifyHandler hace referencia al método SendEmail y lo ejecuta.


Delegates Multicast

Un delegate multicast es un delegate que puede referenciar múltiples métodos al mismo tiempo. En C#, los delegates se vuelven multicast cuando se combinan usando el operador +=.

Cuando se invoca un delegate multicast, cada método en la lista de invocación se ejecuta en el orden en que fue agregado. Esto es especialmente útil para enviar una notificación a múltiples suscriptores.

Ejemplo


using System;

class Program
{
    delegate void Notify(string message);

    static void Main()
    {
        Notify notifier = LogToConsole;
        notifier += LogToFile;
        notifier += SendEmail;

        notifier("Build completado.");
    }

    static void LogToConsole(string message)
    {
        Console.WriteLine("[Console] " + message);
    }

    static void LogToFile(string message)
    {
        Console.WriteLine("[Archivo] " + message);
    }

    static void SendEmail(string message)
    {
        Console.WriteLine("[Email] " + message);
    }
}
  

Un método puede eliminarse de la lista de invocación usando el operador -=:


notifier -= SendEmail;
  
  • Los delegates multicast se utilizan internamente en los events.
  • Si uno de los manejadores lanza una excepción, la ejecución se detiene.
  • Si el delegate tiene un tipo de retorno, solo se devuelve el resultado del último método.

Comportamiento del Valor de Retorno

Si un delegate multicast tiene un tipo de retorno, todos los métodos de la lista de invocación se ejecutan, pero solo se devuelve el resultado del último método.


using System;

class Program
{
    delegate int Calculate(int x);

    static void Main()
    {
        Calculate calc = Square;
        calc += Double;

        int result = calc(5);

        Console.WriteLine(result); 
        // Resultado: 10 (resultado del último método: Double)
    }

    static int Square(int x)
    {
        Console.WriteLine("Square llamado");
        return x * x;
    }

    static int Double(int x)
    {
        Console.WriteLine("Double llamado");
        return x * 2;
    }
}
  

Tanto Square como Double se ejecutan; sin embargo, el delegate devuelve únicamente el resultado del último método agregado.

  • Todos los métodos se ejecutan en orden.
  • Solo se conserva el valor de retorno del último método.
  • Por esta razón, los delegates multicast suelen utilizarse con tipo de retorno void (como en los events).

¿Qué es un Event?

Un event es un mecanismo que señala que una acción ha ocurrido. Los events están construidos sobre delegates y proporcionan acceso controlado. La palabra clave event se utiliza para su declaración, y desde el exterior solo puede accederse mediante += (suscribirse) o -= (cancelar suscripción).

Esto garantiza que:


public class Button
{
    // Definición del event (EventHandler es un delegate estándar)
    public event EventHandler? Click;

    public void SimulateClick()
    {
        Console.WriteLine("¡Botón clicado!");

        // Disparar el event
        Click?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        var btn = new Button();

        // Suscribirse al event
        btn.Click += (s, e) => Console.WriteLine("Event: El botón fue clicado.");

        btn.SimulateClick();
    }
}

El operador condicional nulo (?.) garantiza que el event solo se invoque si existen suscriptores, evitando una NullReferenceException.

En este ejemplo, la clase Button publica un event Click. La expresión lambda suscrita se ejecuta cuando el event se dispara. Este patrón se utiliza ampliamente en frameworks UI como WinForms y WPF.


EventHandler y EventArgs

En C#, el delegate estándar más utilizado para events es EventHandler. Sigue el patrón tradicional: (object sender, EventArgs e).

Al derivar de EventArgs, es posible enviar datos adicionales relacionados con el event. Este enfoque sigue las directrices oficiales de diseño de events en .NET.


public class OrderEventArgs : EventArgs
{
    public int OrderId { get; }
    public decimal Amount { get; }

    public OrderEventArgs(int orderId, decimal amount)
    {
        OrderId = orderId;
        Amount = amount;
    }
}

public class OrderService
{
    public event EventHandler<OrderEventArgs>? OrderCreated;

    public void CreateOrder(int id, decimal amount)
    {
        Console.WriteLine($"Pedido creado (ID={id}, Importe={amount})");

        // Disparar el event
        OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
    }
}

class Program
{
    static void Main()
    {
        var service = new OrderService();

        service.OrderCreated += (s, e) =>
        {
            Console.WriteLine($"Notificación: Pedido recibido (#{e.OrderId}, {e.Amount} USD)");
        };

        service.CreateOrder(101, 250m);
    }
}
  

En este ejemplo, la clase OrderService dispara el event OrderCreated cuando se crea un nuevo pedido. El suscriptor recibe los detalles del pedido a través de la clase personalizada OrderEventArgs.

Este enfoque garantiza seguridad de tipos y hace que la comunicación orientada a eventos sea clara y estructurada.


Delegate vs Event (Encapsulación)

Aunque los events están construidos sobre delegates, no son lo mismo. La diferencia principal radica en la encapsulación y el control de acceso.

Un campo delegate puede modificarse libremente desde fuera de la clase, mientras que un event restringe el acceso externo y protege la lista de invocación.

Ejemplo con Delegate


public class Publisher
{
    public Action? OnChange;
}

Como OnChange es un campo delegate:


publisher.OnChange = null;       // Elimina todos los suscriptores
publisher.OnChange = SomeMethod; // Reemplaza la lista de invocación
publisher.OnChange?.Invoke();    // Puede invocarse externamente

Ejemplo con Event


public class Publisher
{
    public event Action? OnChange;
}

Como OnChange está declarado como un event:

  • Los delegates proporcionan flexibilidad.
  • Los events proporcionan acceso controlado y encapsulación.
  • Para notificaciones públicas, casi siempre se deben preferir events en lugar de campos delegate.

Alternativas modernas: Action & Func

En C# moderno, definir tipos delegate personalizados suele ser innecesario. En su lugar, puedes utilizar los delegates genéricos integrados Action y Func.

Estos delegates son flexibles, concisos y ampliamente utilizados en el ecosistema .NET (incluyendo LINQ, programación asíncrona y APIs basadas en tareas).

Action

Action representa un método que no devuelve ningún valor. Puede aceptar cero o más parámetros.


Action log = message => Console.WriteLine(message);

log("Application started.");

Func

Func<T, TResult> representa un método que devuelve un valor. El último parámetro genérico siempre representa el tipo de retorno.


Func add = (a, b) => a + b;

int result = add(5, 3); // 8
  • Action se utiliza cuando no se necesita valor de retorno.
  • Func se utiliza cuando se requiere un valor de retorno.
  • El uso de estos delegates integrados reduce el código repetitivo.
  • La mayoría de los proyectos C# modernos prefieren Action y Func en lugar de delegates personalizados.

Cancelación de suscripción y fugas de memoria

Un aspecto importante al trabajar con events es la cancelación de suscripción. Si te suscribes a un event y nunca te desuscribes, puede provocar fugas de memoria inesperadas.

Los events mantienen una referencia fuerte a sus suscriptores. Mientras el objeto publisher esté vivo, el subscriber no podrá ser recolectado por el garbage collector.

¿Por qué es un problema?

Imagina un publisher de larga duración (por ejemplo, un servicio o singleton) y un subscriber de corta duración (por ejemplo, un componente UI). Si el subscriber no se desuscribe, permanecerá en memoria incluso cuando ya no sea necesario.


public class Publisher
{
    public event Action? OnChange;

    public void Raise()
    {
        OnChange?.Invoke();
    }
}

public class Subscriber
{
    private readonly Publisher _publisher;

    public Subscriber(Publisher publisher)
    {
        _publisher = publisher;
        _publisher.OnChange += HandleChange;
    }

    private void HandleChange()
    {
        Console.WriteLine("Change detected.");
    }

    public void Dispose()
    {
        // Importante: ¡cancelar suscripción!
        _publisher.OnChange -= HandleChange;
    }
}

Si Dispose nunca se llama, la instancia Subscriber seguirá referenciada por el event y no podrá ser recolectada por el garbage collector.

  • Siempre desuscríbete de los events cuando termine la vida útil del subscriber.
  • Esto es especialmente importante en frameworks UI (WinForms, WPF, Blazor, etc.).
  • Publisher de larga duración + subscriber de corta duración = posible fuga de memoria.
  • Implementar IDisposable es una solución común para una desuscripción segura.

Seguridad en hilos y buenas prácticas

Al trabajar con events en entornos multihilo, la seguridad en hilos se vuelve importante. Un problema común ocurre al verificar null antes de invocar un event.

Posible condición de carrera


if (OnChange != null)
{
    OnChange(this, EventArgs.Empty);
}

Entre la verificación de null y la invocación, otro hilo podría cancelar la suscripción del event. Esto puede provocar una NullReferenceException.

Patrón de invocación seguro

Para evitar esta condición de carrera, copia la referencia del delegate en una variable local antes de invocarlo:


var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);

Esto garantiza que la lista de invocación no cambie entre la verificación y la llamada al método.

Sintaxis moderna

En la mayoría de los casos, el operador condicional nulo ofrece una forma concisa y segura de invocar events:


OnChange?.Invoke(this, EventArgs.Empty);
  • Utiliza patrones de invocación seguros en escenarios multihilo.
  • Prefiere ?.Invoke para una sintaxis moderna y limpia.
  • Los events son delegates multicast — un manejador que falle puede detener la ejecución.
  • Considera aislar las llamadas con try/catch si la confiabilidad es crítica.

¿Cuándo usar Delegates y Events?


TL;DR

  • delegate: Referencias seguras en tipos a métodos con soporte multicast.
  • event: Delegate controlado que permite suscripción y preserva la encapsulación.
  • EventHandler y EventArgs: Siguen el patrón estándar de events en .NET para pasar datos.
  • Action y Func: Alternativas modernas a delegates personalizados.
  • Desuscríbete de los events cuando sea necesario para evitar fugas de memoria.
  • Usa events para notificaciones y delegates (o Action/Func) para callbacks y control de flujo.

Artículos relacionados