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ícateEventos 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:
- Solo pueden ser invocados desde la clase que los declara
- Proporcionan operadores especiales (
+=
y-=
) para suscripción y cancelación - 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.
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
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.
CRUD en C# de modelo Customer sobre una lista
Arrays y listas
Objetos
Excepciones
Eventos
Lambdas
Diccionarios en C#
Variables y constantes
Tipos de datos
Herencia
Operadores
Uso de consultas LINQ
Clases y encapsulación
Uso de consultas LINQ
Excepciones
Control de flujo
Eventos
Diccionarios
Tipos de datos
Conjuntos, colas y pilas
Lambdas
Conjuntos, colas y pilas
Uso de async y await
Tareas
Constructores y destructores
Operadores
Arrays y listas
Polimorfismo
Polimorfismo
Variables y constantes
Proyecto colecciones y LINQ en C#
Clases y encapsulación
Creación de proyecto C#
Uso de async y await
Funciones
Delegados
Delegados
Constructores y destructores
Objetos
Control de flujo
Funciones
Tareas
Proyecto sintaxis en C#
Herencia C Sharp
OOP en C Sharp
Diccionarios
Introducción a C#
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.