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

  1. El problema: avisar sin acoplar quién avisa con quién escucha
  2. Delegados: variables que apuntan a métodos
  3. Delegados predefinidos: Action, Func y Predicate
  4. Multidifusión: un delegado con varios métodos suscritos
  5. Eventos: event y el patrón publicador/suscriptor
  6. event frente a un delegado público a secas
  7. Aplicando eventos en BiblioTech: PrestamoRegistrado

  1. 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.

  1. 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:

delegate bool FiltroMaterial(MaterialBibliotecario material);

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 delegado

filtro(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á.

  1. Delegados predefinidos: Action, Func y Predicate

Declarar 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");

  1. 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.

  1. Eventos: event y el patrón publicador/suscriptor

Un 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.

class Biblioteca
{
    public event Action<Prestamo> PrestamoRegistrado;
}
  • La clase Biblioteca es 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 "="

  1. event frente a un delegado público a secas

public Action<Prestamo> Campo; public event Action<Prestamo> Evento;
Suscribir con += desde fuera
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 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.

  1. Aplicando eventos en BiblioTech: PrestamoRegistrado

Con 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:

Registrando prestamo de 'Rayuela'...
Se ha prestado 'Rayuela' a Ana Martinez

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 una NullReferenceException; usa siempre PrestamoRegistrado?.Invoke(...) dentro de la clase publicadora.
  • Intentar invocar un evento desde fuera de la clase: biblioteca.PrestamoRegistrado(p) no compila; solo la propia clase Biblioteca puede invocar su evento. Si necesitas que algo externo dispare la lógica, expón un método público (como RegistrarPrestamo) 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> (o EventHandler<T> en APIs más tradicionales de .NET) es casi siempre la elección correcta.

Ejercicios

  1. Declara un delegado Func<MaterialBibliotecario, bool> llamado filtro que apunte a un método EsLibroDisponible(MaterialBibliotecario material) (devuelve true si el material es un Libro y está Disponible). Invócalo sobre dos materiales distintos y muestra el resultado.

  2. Crea una clase Biblioteca con un event Action<Prestamo> PrestamoRegistrado y un método RegistrarPrestamo(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.

  3. 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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados