En la lección anterior formalizaste el "qué puede hacer" un objeto con interfaces. Esta
lección resuelve una pregunta distinta: cómo hacer que una parte del programa avise a
otra de que algo ha ocurrido —por ejemplo, que se ha registrado un préstamo—, sin que quien
provoca el aviso necesite conocer de antemano quién va a reaccionar, ni cuántos. La solución de
C# se apoya en dos piezas relacionadas: los delegados, variables que almacenan una
referencia a un método, y los eventos, construidos sobre delegados, que formalizan el
patrón "publicador/suscriptor". En esta lección conocerás ambos y darás el primer paso a la
clase Biblioteca, que en las próximas lecciones organizará todo el catálogo de BiblioTech.
Contenido
- El problema: avisar sin acoplar quién avisa con quién escucha
- Delegados: variables que apuntan a métodos
- Delegados predefinidos:
Action,FuncyPredicate - Multidifusión: un delegado con varios métodos suscritos
- Eventos:
eventy el patrón publicador/suscriptor eventfrente a un delegado público a secas- Aplicando eventos en BiblioTech:
PrestamoRegistrado
- El problema: avisar sin acoplar quién avisa con quién escucha
Imagina que, al registrarse un préstamo en BiblioTech, quieres que ocurran varias cosas a la vez: mostrar un mensaje de confirmación, quizás en el futuro enviar una notificación por correo, quizás registrar el préstamo en un archivo de auditoría. La forma directa —llamar a cada una de esas acciones explícitamente dentro del método que registra el préstamo— funciona, pero acopla fuertemente la lógica de "registrar un préstamo" con la de "todas las cosas que deben pasar después", y cada vez que se añade una notificación nueva hay que volver a tocar ese método. Los delegados y los eventos permiten invertir esa relación: el código que registra el préstamo simplemente "avisa de que algo ha pasado", sin saber ni importarle quién (ninguno, uno o varios) está escuchando ese aviso.
- Delegados: variables que apuntan a métodos
Un delegado es un tipo que representa la firma de un método (sus parámetros y su tipo de retorno) y cuyas variables pueden almacenar una referencia a cualquier método compatible con esa firma:
Esta línea declara un tipo delegado llamado FiltroMaterial: cualquier método que reciba un
MaterialBibliotecario y devuelva un bool es compatible con él. Una variable de este tipo
puede apuntar a cualquiera de esos métodos:
bool EsRevista(MaterialBibliotecario material)
{
return material is Revista;
}
FiltroMaterial filtro = EsRevista; // la variable "filtro" ahora apunta al metodo EsRevista
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(filtro(libro1)); // False: invoca EsRevista(libro1) a traves del delegadofiltro(libro1) no llama a ningún método llamado filtro: invoca, a través del delegado, el
método al que filtro apunta en ese momento (EsRevista). Esto es lo que hace útil a un
delegado: se puede pasar como parámetro, guardar en una variable o en un campo, y cambiar en
tiempo de ejecución qué método concreto se ejecutará.
- Delegados predefinidos:
Action, Func y Predicate
Action, Func y PredicateDeclarar un delegate propio para cada firma distinta sería repetitivo. .NET ofrece tres
familias de delegados genéricos ya predefinidos (los genéricos, con <T>, se estudian en
profundidad en la próxima lección) que cubren casi todos los casos habituales:
| Delegado | Parámetros | Valor de retorno | Ejemplo de firma |
|---|---|---|---|
Action |
Cero o más (hasta 16) | Ninguno (void) |
Action<Prestamo> → void M(Prestamo p) |
Func<..., TResultado> |
Cero o más | El último parámetro de tipo genérico | Func<MaterialBibliotecario, bool> → bool M(MaterialBibliotecario m) |
Predicate<T> |
Exactamente uno | bool |
Predicate<MaterialBibliotecario> → bool M(MaterialBibliotecario m) |
En la práctica, Predicate<T> y Func<T, bool> son equivalentes (mismos parámetros, mismo
retorno); Predicate<T> es más antiguo y se ve sobre todo en APIs heredadas, mientras que
Func/Action son los que verás con más frecuencia en código moderno y en LINQ (lección
posterior de este módulo). Con estos delegados predefinidos, ya no hace falta declarar
FiltroMaterial:
Func<MaterialBibliotecario, bool> esRevista = EsRevista; // mismo metodo, delegado predefinido
Action<string> mostrarMensaje = Console.WriteLine; // Action que envuelve un metodo de la propia BCL
mostrarMensaje("Aviso desde un Action");
- Multidifusión: un delegado con varios métodos suscritos
Una característica clave de los delegados en C# es que no están limitados a apuntar a un
único método: con += se puede "suscribir" más de uno, y todos se ejecutan, en orden, al
invocar el delegado (esto se llama multidifusión, multicast):
Action<string> notificaciones = null;
notificaciones += mensaje => Console.WriteLine($"[Log] {mensaje}");
notificaciones += mensaje => Console.WriteLine($"[Consola] {mensaje}");
notificaciones("Prestamo registrado");
// [Log] Prestamo registrado
// [Consola] Prestamo registrado-= retira un método previamente suscrito. Si un delegado de tipo Action (sin valor de
retorno) es null porque nadie se ha suscrito todavía, invocarlo directamente lanzaría una
excepción; por eso es habitual comprobar antes con ?.Invoke(...), como verás en el apartado
7.
- Eventos:
event y el patrón publicador/suscriptor
event y el patrón publicador/suscriptorUn delegado público a secas tiene un inconveniente: cualquier código externo podría no solo
suscribirse, sino también reemplazar por completo la lista de suscriptores (con = en
lugar de +=) o incluso invocarlo directamente desde fuera, rompiendo la intención de que solo
el propio objeto decida cuándo "avisar". La palabra clave event resuelve esto: declara un
delegado con reglas más estrictas, pensadas exactamente para el patrón
publicador/suscriptor.
- La clase
Bibliotecaes la publicadora: es la única que puede invocar el evento (PrestamoRegistrado?.Invoke(...)), siempre desde dentro de la propia clase. - Cualquier otro código puede ser suscriptor: puede añadir (
+=) o quitar (-=) un método que reaccione al evento, pero no puede invocarlo directamente ni reemplazar toda la lista de suscriptores con=.
void NotificarPrestamo(Prestamo prestamo)
{
Console.WriteLine($"Se ha prestado '{prestamo.Libro.Titulo}' a {prestamo.Socio.Nombre}");
}
Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += NotificarPrestamo; // suscripcion valida desde fuera de la clase
// biblioteca.PrestamoRegistrado(prestamo1); // Error de compilacion: no se puede invocar un event desde fuera
// biblioteca.PrestamoRegistrado = NotificarPrestamo; // Error de compilacion: no se puede reemplazar con "="
event frente a un delegado público a secas
event frente a un delegado público a secaspublic Action<Prestamo> Campo; |
public event Action<Prestamo> Evento; |
|
|---|---|---|
Suscribir con += desde fuera |
Sí | Sí |
Reemplazar toda la lista con = desde fuera |
Sí (riesgo de borrar otros suscriptores por error) | No, error de compilación |
| Invocar directamente desde fuera de la clase | Sí | No, error de compilación |
| Intención que comunica al leer el código | Ambigua: parece un campo cualquiera | Clara: "esto es un aviso al que te puedes suscribir" |
En la práctica, siempre que el propósito sea "avisar de que algo ha pasado" (un préstamo
registrado, una devolución, un material añadido al catálogo), event es la elección correcta;
un delegado público sin event rara vez se justifica fuera de casos muy concretos.
- Aplicando eventos en BiblioTech:
PrestamoRegistrado
PrestamoRegistradoCon lo aprendido, esta es la primera versión de la clase Biblioteca (una versión mínima,
centrada solo en el evento; en la próxima lección, de Colecciones, se ampliará con el
catálogo completo de materiales y socios):
class Biblioteca
{
public event Action<Prestamo> PrestamoRegistrado;
public void RegistrarPrestamo(Prestamo prestamo)
{
Console.WriteLine($"Registrando prestamo de '{prestamo.Libro.Titulo}'...");
PrestamoRegistrado?.Invoke(prestamo); // el "?." evita una excepcion si nadie se ha suscrito
}
}Un suscriptor típico, pensado como un pequeño "log de notificaciones":
void LogDeNotificaciones(Prestamo prestamo)
{
Console.WriteLine($"Se ha prestado '{prestamo.Libro.Titulo}' a {prestamo.Socio.Nombre}");
}Y el programa que conecta ambas piezas:
Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += LogDeNotificaciones;
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Socio socio1 = new Socio(1, "Ana Martinez");
libro1.Prestar();
Prestamo prestamo1 = new Prestamo(libro1, socio1);
biblioteca.RegistrarPrestamo(prestamo1);Salida:
RegistrarPrestamo no conoce LogDeNotificaciones ni sabe nada de él; simplemente invoca su
evento, y quien se haya suscrito (uno, varios, o ninguno) reacciona. Se podría añadir
mañana un segundo suscriptor —por ejemplo, uno que en el Módulo 5 escriba el préstamo en un
archivo— sin tocar ni una línea de Biblioteca ni de RegistrarPrestamo.
Errores Comunes y Consejos
- Invocar un evento sin comprobar
null: si nadie se ha suscrito todavía,PrestamoRegistrado.Invoke(...)(sin?.) lanza unaNullReferenceException; usa siemprePrestamoRegistrado?.Invoke(...)dentro de la clase publicadora. - Intentar invocar un evento desde fuera de la clase:
biblioteca.PrestamoRegistrado(p)no compila; solo la propia claseBibliotecapuede invocar su evento. Si necesitas que algo externo dispare la lógica, expón un método público (comoRegistrarPrestamo) que internamente invoque el evento. - Olvidar desuscribirse (
-=) cuando ya no hace falta: si un objeto se suscribe a un evento de otro objeto de vida más larga y nunca se desuscribe, el publicador mantiene una referencia al suscriptor indefinidamente, lo que puede impedir que se libere memoria (una introducción a este tema; se retoma con más detalle en el Módulo 6, Gestión de Memoria y GC). - Confundir el orden de ejecución de varios suscriptores con garantías fuertes: aunque en la
práctica se ejecutan en el orden en que se suscribieron con
+=, no conviene diseñar lógica que dependa críticamente de ese orden entre suscriptores independientes. - Consejo: si un método solo necesita informar de un cambio, sin esperar ningún valor de
vuelta útil,
event Action<T>(oEventHandler<T>en APIs más tradicionales de .NET) es casi siempre la elección correcta.
Ejercicios
-
Declara un delegado
Func<MaterialBibliotecario, bool>llamadofiltroque apunte a un métodoEsLibroDisponible(MaterialBibliotecario material)(devuelvetruesi el material es unLibroy estáDisponible). Invócalo sobre dos materiales distintos y muestra el resultado. -
Crea una clase
Bibliotecacon unevent Action<Prestamo> PrestamoRegistradoy un métodoRegistrarPrestamo(Prestamo prestamo)que lo invoque. Suscribe dos métodos distintos al evento (uno que muestre un mensaje de confirmación, otro que muestre el nombre del socio) y comprueba que ambos se ejecutan al registrar un préstamo. -
Añade a la solución del ejercicio anterior un tercer suscriptor, y luego elimínalo con
-=antes de registrar un segundo préstamo. Comprueba que, en el segundo préstamo, ese tercer suscriptor ya no se ejecuta.
Soluciones
bool EsLibroDisponible(MaterialBibliotecario material)
{
return material is Libro && material.Disponible;
}
Func<MaterialBibliotecario, bool> filtro = EsLibroDisponible;
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
Console.WriteLine(filtro(libro1)); // True
Console.WriteLine(filtro(revista1)); // False: no es un Libro
class Biblioteca
{
public event Action<Prestamo> PrestamoRegistrado;
public void RegistrarPrestamo(Prestamo prestamo)
{
PrestamoRegistrado?.Invoke(prestamo);
}
}
void Confirmacion(Prestamo p) => Console.WriteLine($"Prestamo de '{p.Libro.Titulo}' confirmado.");
void MostrarSocio(Prestamo p) => Console.WriteLine($"Socio: {p.Socio.Nombre}");
Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += Confirmacion;
biblioteca.PrestamoRegistrado += MostrarSocio;
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Socio socio1 = new Socio(1, "Ana Martinez");
libro1.Prestar();
biblioteca.RegistrarPrestamo(new Prestamo(libro1, socio1));
// Prestamo de 'Rayuela' confirmado.
// Socio: Ana Martinez
void AvisoTemporal(Prestamo p) => Console.WriteLine("Aviso temporal de prueba");
biblioteca.PrestamoRegistrado += AvisoTemporal;
// ... se ejecutarian los tres suscriptores en el siguiente prestamo ...
biblioteca.PrestamoRegistrado -= AvisoTemporal; // se retira antes del segundo prestamo
Libro libro2 = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Socio socio2 = new Socio(2, "Luis Gomez");
libro2.Prestar();
biblioteca.RegistrarPrestamo(new Prestamo(libro2, socio2));
// Solo se ejecutan Confirmacion y MostrarSocio; AvisoTemporal ya no se ejecuta.
Conclusión
En esta lección has aprendido qué es un delegado, cómo usar los predefinidos Action, Func
y Predicate, cómo un mismo delegado puede tener varios métodos suscritos a la vez
(multidifusión), y cómo event restringe esa capacidad al patrón publicador/suscriptor,
evitando invocaciones o reemplazos indebidos desde fuera de la clase. Ha nacido la clase
Biblioteca, con su evento PrestamoRegistrado, que en las próximas lecciones de este módulo
crecerá hasta convertirse en el punto central de todo el catálogo de BiblioTech.
Los delegados y eventos resuelven "cómo reaccionar a algo que ya pasó". La próxima lección
cambia de tema hacia otra parte fundamental del C# moderno: el pattern matching, que te
permitirá expresar de forma mucho más compacta y legible las comprobaciones de tipo (is,
as) que ya usaste en la lección de Polimorfismo, junto con las switch expressions y los
nullable reference types.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
