CSharp

Tutorial CSharp: Eventos

Aprende cómo usar eventos y delegados en C# para implementar el patrón publisher-subscriber y mejorar la comunicación entre objetos.

Aprende CSharp y certifícate

Eventos vs delegados

En C#, los eventos y delegados son mecanismos fundamentales para implementar la comunicación entre objetos. Aunque están estrechamente relacionados, tienen propósitos y comportamientos diferentes que es importante comprender.

Los delegados son tipos que representan referencias a métodos con una firma específica. Funcionan como "punteros a funciones" tipados y seguros, permitiendo pasar métodos como parámetros. Por otro lado, los eventos son una construcción basada en delegados que implementa el patrón de diseño Observer, proporcionando un mecanismo para notificar a múltiples objetos cuando ocurre una acción.

Delegados: la base de los eventos

Un delegado en C# define un tipo que especifica una firma de método particular:

// Definición de un delegado
public delegate void MensajeHandler(string mensaje);

Este delegado puede referenciar cualquier método que acepte un string y no devuelva nada:

public class Emisor
{
    // Creamos una variable del tipo delegado
    public MensajeHandler ManejadorDeMensaje;
    
    public void EnviarMensaje(string mensaje)
    {
        // Invocamos el delegado si tiene suscriptores
        if (ManejadorDeMensaje != null)
        {
            ManejadorDeMensaje(mensaje);
        }
    }
}

Para usar este delegado, asignamos métodos que coincidan con su firma:

public class Receptor
{
    public void RecibirMensaje(string mensaje)
    {
        Console.WriteLine($"Mensaje recibido: {mensaje}");
    }
}

// Uso
Emisor emisor = new Emisor();
Receptor receptor = new Receptor();

// Asignamos el método al delegado
emisor.ManejadorDeMensaje = receptor.RecibirMensaje;

// Invocamos el delegado indirectamente
emisor.EnviarMensaje("Hola mundo");

Eventos: delegados con protección

Los eventos son una capa de abstracción sobre los delegados que añade protección y sigue el patrón publisher-subscriber. La principal diferencia es que los eventos:

  1. Solo pueden ser invocados desde la clase que los declara
  2. Proporcionan operadores especiales (+= y -=) para suscripción y cancelación
  3. No pueden ser asignados directamente (no se puede usar =)

Veamos cómo convertir nuestro ejemplo anterior para usar eventos:

public class EmisorConEvento
{
    // Declaramos un evento basado en el delegado
    public event MensajeHandler OnMensajeEnviado;
    
    public void EnviarMensaje(string mensaje)
    {
        // Patrón seguro para invocar eventos
        OnMensajeEnviado?.Invoke(mensaje);
    }
}

El uso de eventos es similar pero con algunas diferencias clave:

EmisorConEvento emisor = new EmisorConEvento();
Receptor receptor = new Receptor();

// Nos suscribimos al evento
emisor.OnMensajeEnviado += receptor.RecibirMensaje;

// Esto generaría un error de compilación:
// emisor.OnMensajeEnviado = receptor.RecibirMensaje;

// Invocamos el evento indirectamente
emisor.EnviarMensaje("Hola mundo");

// Podemos cancelar la suscripción
emisor.OnMensajeEnviado -= receptor.RecibirMensaje;

Diferencias clave entre eventos y delegados

Característica Delegados Eventos
Propósito principal Referencias a métodos Notificaciones entre objetos
Acceso Pueden ser invocados por cualquier código con acceso Solo pueden ser invocados por la clase que los declara
Asignación Permiten asignación directa (=) Solo permiten operaciones de suscripción (+=, -=)
Encapsulación Menor (cualquiera puede invocar o reasignar) Mayor (protege la lista de suscriptores)
Multicast Soportado, pero sin protección Soportado y protegido

Cuándo usar cada uno

  • Usa delegados cuando:
  • Necesites pasar métodos como parámetros
  • Requieras flexibilidad para reasignar completamente la referencia
  • Estés implementando callbacks simples dentro de una clase
  • Usa eventos cuando:
  • Implementes el patrón Observer/Publisher-Subscriber
  • Necesites notificar a múltiples objetos sobre un cambio
  • Quieras proteger la lista de suscriptores
  • Desees seguir las convenciones de .NET para comunicación entre objetos

Ejemplo práctico: Sistema de notificaciones

Veamos un ejemplo más completo que muestra las ventajas de los eventos sobre los delegados simples:

// Definimos un delegado con información del evento
public delegate void CambioTemperaturaHandler(object sender, double nuevaTemperatura);

public class SensorTemperatura
{
    private double _temperatura;
    
    // Declaramos un evento basado en el delegado
    public event CambioTemperaturaHandler OnCambioTemperatura;
    
    public double Temperatura
    {
        get { return _temperatura; }
        set
        {
            if (_temperatura != value)
            {
                _temperatura = value;
                // Notificamos a todos los suscriptores
                OnCambioTemperatura?.Invoke(this, _temperatura);
            }
        }
    }
}

// Clases que responden al evento
public class PantallaTemperatura
{
    public void MostrarTemperatura(object sender, double temperatura)
    {
        Console.WriteLine($"Temperatura actual: {temperatura}°C");
    }
}

public class SistemaAlarma
{
    public void VerificarTemperatura(object sender, double temperatura)
    {
        if (temperatura > 30)
        {
            Console.WriteLine("¡ALERTA! Temperatura demasiado alta");
        }
    }
}

Y así se utilizaría:

// Creamos los objetos
SensorTemperatura sensor = new SensorTemperatura();
PantallaTemperatura pantalla = new PantallaTemperatura();
SistemaAlarma alarma = new SistemaAlarma();

// Suscribimos múltiples manejadores al mismo evento
sensor.OnCambioTemperatura += pantalla.MostrarTemperatura;
sensor.OnCambioTemperatura += alarma.VerificarTemperatura;

// Al cambiar la temperatura, se notifica automáticamente a todos los suscriptores
sensor.Temperatura = 25.5;  // Muestra: "Temperatura actual: 25.5°C"
sensor.Temperatura = 32.0;  // Muestra ambos mensajes, incluyendo la alerta

Este ejemplo muestra cómo los eventos facilitan la implementación del patrón Observer, permitiendo que múltiples objetos (la pantalla y la alarma) respondan a cambios en un objeto observado (el sensor) sin que estos objetos tengan que conocerse entre sí.

Los eventos proporcionan un nivel de encapsulación y seguridad que los delegados por sí solos no ofrecen, haciendo que sean la opción preferida para la comunicación entre componentes en aplicaciones C#.

Patrón publisher-subscriber

El patrón publisher-subscriber (también conocido como pub-sub) es un modelo de comunicación donde los componentes interactúan sin acoplarse directamente entre sí. Este patrón es fundamental en el desarrollo de aplicaciones C# modernas y constituye la base conceptual de los eventos que hemos visto anteriormente.

En este patrón, existen dos tipos principales de participantes:

  • Publishers (publicadores): componentes que generan mensajes o notificaciones
  • Subscribers (suscriptores): componentes que reciben y procesan esos mensajes

La característica más importante del patrón pub-sub es que los publicadores no necesitan conocer quiénes son sus suscriptores, lo que permite un bajo acoplamiento entre componentes.

Estructura básica del patrón

En C#, este patrón se implementa naturalmente mediante el sistema de eventos. Veamos su estructura básica:

public class Publisher
{
    // El evento que los suscriptores pueden escuchar
    public event EventHandler<MiEventoArgs> AlgoSucedio;

    // Método que dispara el evento
    protected virtual void OnAlgoSucedio(MiEventoArgs e)
    {
        // Invoca el evento de manera segura
        AlgoSucedio?.Invoke(this, e);
    }

    // Método que realiza alguna acción y luego notifica
    public void RealizarAccion()
    {
        // Lógica de la acción
        Console.WriteLine("Acción realizada");
        
        // Notificar a los suscriptores
        OnAlgoSucedio(new MiEventoArgs("Datos del evento"));
    }
}

// Clase para transportar datos del evento
public class MiEventoArgs : EventArgs
{
    public string Mensaje { get; }
    
    public MiEventoArgs(string mensaje)
    {
        Mensaje = mensaje;
    }
}

public class Subscriber
{
    public void Suscribirse(Publisher publicador)
    {
        // Nos suscribimos al evento
        publicador.AlgoSucedio += ManejarEvento;
    }
    
    public void Desuscribirse(Publisher publicador)
    {
        // Cancelamos la suscripción
        publicador.AlgoSucedio -= ManejarEvento;
    }
    
    private void ManejarEvento(object sender, MiEventoArgs e)
    {
        Console.WriteLine($"Evento recibido: {e.Mensaje}");
    }
}

Ventajas del patrón publisher-subscriber

Este patrón ofrece varias ventajas significativas:

  • Desacoplamiento: Los publicadores y suscriptores no necesitan conocerse mutuamente
  • Escalabilidad: Se pueden añadir nuevos suscriptores sin modificar el publicador
  • Flexibilidad: Los suscriptores pueden registrarse y cancelar su suscripción dinámicamente
  • Mantenibilidad: Facilita los cambios en el sistema sin afectar a todos los componentes

Implementación en un escenario real

Veamos un ejemplo práctico de cómo implementar este patrón en un escenario de comercio electrónico:

// Datos del evento de pedido
public class PedidoEventArgs : EventArgs
{
    public int PedidoId { get; }
    public string Cliente { get; }
    public decimal Total { get; }
    
    public PedidoEventArgs(int pedidoId, string cliente, decimal total)
    {
        PedidoId = pedidoId;
        Cliente = cliente;
        Total = total;
    }
}

// Publisher - Sistema de pedidos
public class SistemaPedidos
{
    // Evento que se dispara cuando se crea un nuevo pedido
    public event EventHandler<PedidoEventArgs> PedidoCreado;
    
    // Método para crear un nuevo pedido
    public void CrearPedido(int pedidoId, string cliente, decimal total)
    {
        // Lógica para crear el pedido en la base de datos
        Console.WriteLine($"Pedido {pedidoId} creado en el sistema");
        
        // Notificar a todos los suscriptores
        OnPedidoCreado(new PedidoEventArgs(pedidoId, cliente, total));
    }
    
    // Método protegido para disparar el evento
    protected virtual void OnPedidoCreado(PedidoEventArgs e)
    {
        PedidoCreado?.Invoke(this, e);
    }
}

// Subscriber 1 - Sistema de notificaciones por email
public class ServicioEmail
{
    public void Inicializar(SistemaPedidos sistemaPedidos)
    {
        sistemaPedidos.PedidoCreado += EnviarEmailConfirmacion;
    }
    
    private void EnviarEmailConfirmacion(object sender, PedidoEventArgs e)
    {
        // En un sistema real, aquí enviaríamos un email
        Console.WriteLine($"Email enviado a {e.Cliente}: Confirmación del pedido #{e.PedidoId}");
    }
}

// Subscriber 2 - Sistema de inventario
public class SistemaInventario
{
    public void Inicializar(SistemaPedidos sistemaPedidos)
    {
        sistemaPedidos.PedidoCreado += ActualizarInventario;
    }
    
    private void ActualizarInventario(object sender, PedidoEventArgs e)
    {
        Console.WriteLine($"Inventario actualizado para el pedido #{e.PedidoId}");
    }
}

// Subscriber 3 - Sistema de análisis de ventas
public class AnalisisVentas
{
    public void Inicializar(SistemaPedidos sistemaPedidos)
    {
        sistemaPedidos.PedidoCreado += RegistrarVenta;
    }
    
    private void RegistrarVenta(object sender, PedidoEventArgs e)
    {
        Console.WriteLine($"Venta de ${e.Total} registrada en análisis");
    }
}

Para utilizar este sistema:

// Creamos el publicador
SistemaPedidos sistemaPedidos = new SistemaPedidos();

// Creamos los suscriptores
ServicioEmail servicioEmail = new ServicioEmail();
SistemaInventario sistemaInventario = new SistemaInventario();
AnalisisVentas analisisVentas = new AnalisisVentas();

// Inicializamos los suscriptores (se suscriben al evento)
servicioEmail.Inicializar(sistemaPedidos);
sistemaInventario.Inicializar(sistemaPedidos);
analisisVentas.Inicializar(sistemaPedidos);

// Creamos un pedido - esto disparará el evento
sistemaPedidos.CrearPedido(1001, "Ana García", 149.99m);

Al ejecutar este código, veremos cómo un solo evento (la creación del pedido) desencadena múltiples acciones en diferentes sistemas, sin que estos sistemas tengan que conocerse entre sí.

Consideraciones de diseño

Al implementar el patrón publisher-subscriber en C#, es importante tener en cuenta:

  • Rendimiento: Si hay muchos suscriptores o el evento se dispara con frecuencia, puede afectar al rendimiento
  • Manejo de errores: Los errores en un suscriptor no deberían afectar a otros suscriptores
  • Orden de ejecución: No se garantiza un orden específico en la ejecución de los manejadores
  • Ciclo de vida: Es importante desuscribirse de los eventos cuando ya no se necesitan para evitar fugas de memoria

Implementación con interfaces

Para aplicaciones más complejas, podemos formalizar el patrón mediante interfaces:

// Interfaz para los publicadores
public interface IPublisher<T> where T : EventArgs
{
    event EventHandler<T> EventoPublicado;
}

// Interfaz para los suscriptores
public interface ISubscriber<T> where T : EventArgs
{
    void Manejar(object sender, T args);
}

// Implementación concreta de un publicador
public class NotificadorCambios : IPublisher<CambioEventArgs>
{
    public event EventHandler<CambioEventArgs> EventoPublicado;
    
    public void RealizarCambio(string descripcion)
    {
        Console.WriteLine($"Cambio realizado: {descripcion}");
        EventoPublicado?.Invoke(this, new CambioEventArgs(descripcion));
    }
}

// Implementación concreta de un suscriptor
public class RegistroCambios : ISubscriber<CambioEventArgs>
{
    public void Suscribirse(IPublisher<CambioEventArgs> publicador)
    {
        publicador.EventoPublicado += Manejar;
    }
    
    public void Manejar(object sender, CambioEventArgs args)
    {
        Console.WriteLine($"Cambio registrado: {args.Descripcion} a las {DateTime.Now}");
    }
}

// Clase para los datos del evento
public class CambioEventArgs : EventArgs
{
    public string Descripcion { get; }
    
    public CambioEventArgs(string descripcion)
    {
        Descripcion = descripcion;
    }
}

Este enfoque basado en interfaces facilita la implementación de pruebas unitarias y permite una mayor flexibilidad en el diseño del sistema.

Eventos estándar en .NET

El framework .NET incluye varios patrones estándar para eventos que siguen el modelo publisher-subscriber:

  • EventHandler: Para eventos sin datos adicionales
  • EventHandler: Para eventos con datos personalizados
  • PropertyChangedEventHandler: Para notificar cambios en propiedades (usado en INotifyPropertyChanged)

Estos tipos de eventos estándar facilitan la implementación consistente del patrón en toda la aplicación.

El patrón publisher-subscriber, implementado a través de eventos en C#, proporciona una forma elegante y efectiva de comunicación entre componentes que promueve un diseño de software más modular y mantenible.

Aprende CSharp online

Otras lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

Ejercicios de programación de CSharp

Evalúa tus conocimientos de esta lección Eventos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la diferencia entre delegados y eventos en C#.
  • Aprender a declarar, suscribir e invocar eventos y delegados.
  • Entender el patrón publisher-subscriber y su implementación mediante eventos.
  • Conocer las ventajas y consideraciones al usar eventos para comunicación entre componentes.
  • Aplicar eventos en escenarios prácticos para notificaciones y desacoplamiento de componentes.