A la lliçó anterior vas formalitzar el "què pot fer" un objecte amb interfícies. Aquesta lliçó resol una pregunta diferent: com fer que una part del programa avisi una altra que ha passat alguna cosa —per exemple, que s'ha registrat un préstec—, sense que qui provoca l'avís necessiti conèixer d'entrada qui hi reaccionarà, ni quants. La solució de C# es recolza en dues peces relacionades: els delegats, variables que emmagatzemen una referència a un mètode, i els esdeveniments, construïts sobre delegats, que formalitzen el patró "publicador/subscriptor". En aquesta lliçó coneixeràs totes dues i faràs el primer pas cap a la classe Biblioteca, que a les properes lliçons organitzarà tot el catàleg de BiblioTech.

Contingut

  1. El problema: avisar sense acoblar qui avisa amb qui escolta
  2. Delegats: variables que apunten a mètodes
  3. Delegats predefinits: Action, Func i Predicate
  4. Multidifusió: un delegat amb diversos mètodes subscrits
  5. Esdeveniments: event i el patró publicador/subscriptor
  6. event enfront d'un delegat públic a seques
  7. Aplicant esdeveniments a BiblioTech: PrestecRegistrat

  1. El problema: avisar sense acoblar qui avisa amb qui escolta

Imagina't que, en registrar-se un préstec a BiblioTech, vols que passin diverses coses alhora: mostrar un missatge de confirmació, potser en el futur enviar una notificació per correu, potser registrar el préstec en un arxiu d'auditoria. La forma directa —cridar cadascuna d'aquestes accions explícitament dins del mètode que registra el préstec— funciona, però acobla fortament la lògica de "registrar un préstec" amb la de "totes les coses que han de passar després", i cada vegada que s'afegeix una notificació nova cal tornar a tocar aquest mètode. Els delegats i els esdeveniments permeten invertir aquesta relació: el codi que registra el préstec simplement "avisa que ha passat alguna cosa", sense saber ni importar-li qui (cap, un o diversos) està escoltant aquest avís.

  1. Delegats: variables que apunten a mètodes

Un delegat és un tipus que representa la signatura d'un mètode (els seus paràmetres i el seu tipus de retorn) i les variables del qual poden emmagatzemar una referència a qualsevol mètode compatible amb aquesta signatura:

delegate bool FiltreMaterial(MaterialBibliotecari material);

Aquesta línia declara un tipus delegat anomenat FiltreMaterial: qualsevol mètode que rebi un MaterialBibliotecari i retorni un bool és compatible amb ell. Una variable d'aquest tipus pot apuntar a qualsevol d'aquests mètodes:

bool EsRevista(MaterialBibliotecari material)
{
    return material is Revista;
}

FiltreMaterial filtre = EsRevista; // la variable "filtre" ara apunta al metode EsRevista

Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(filtre(llibre1)); // False: invoca EsRevista(llibre1) a traves del delegat

filtre(llibre1) no crida cap mètode anomenat filtre: invoca, a través del delegat, el mètode al qual filtre apunta en aquell moment (EsRevista). Això és el que fa útil un delegat: es pot passar com a paràmetre, guardar en una variable o en un camp, i canviar en temps d'execució quin mètode concret s'executarà.

  1. Delegats predefinits: Action, Func i Predicate

Declarar un delegate propi per a cada signatura diferent seria repetitiu. .NET ofereix tres famílies de delegats genèrics ja predefinits (els genèrics, amb <T>, s'estudien en profunditat a la propera lliçó) que cobreixen gairebé tots els casos habituals:

Delegat Paràmetres Valor de retorn Exemple de signatura
Action Zero o més (fins a 16) Cap (void) Action<Prestec>void M(Prestec p)
Func<..., TResultat> Zero o més L'últim paràmetre de tipus genèric Func<MaterialBibliotecari, bool>bool M(MaterialBibliotecari m)
Predicate<T> Exactament un bool Predicate<MaterialBibliotecari>bool M(MaterialBibliotecari m)

A la pràctica, Predicate<T> i Func<T, bool> són equivalents (mateixos paràmetres, mateix retorn); Predicate<T> és més antic i es veu sobretot en API heretades, mentre que Func/ Action són els que veuràs amb més freqüència en codi modern i en LINQ (lliçó posterior d'aquest mòdul). Amb aquests delegats predefinits, ja no cal declarar FiltreMaterial:

Func<MaterialBibliotecari, bool> esRevista = EsRevista; // mateix metode, delegat predefinit

Action<string> mostrarMissatge = Console.WriteLine; // Action que embolica un metode de la propia BCL
mostrarMissatge("Avis des d'un Action");

  1. Multidifusió: un delegat amb diversos mètodes subscrits

Una característica clau dels delegats en C# és que no estan limitats a apuntar a un únic mètode: amb += es pot "subscriure" més d'un, i tots s'executen, en ordre, en invocar el delegat (això s'anomena multidifusió, multicast):

Action<string> notificacions = null;
notificacions += missatge => Console.WriteLine($"[Log] {missatge}");
notificacions += missatge => Console.WriteLine($"[Consola] {missatge}");

notificacions("Prestec registrat");
// [Log] Prestec registrat
// [Consola] Prestec registrat

-= retira un mètode prèviament subscrit. Si un delegat de tipus Action (sense valor de retorn) és null perquè encara ningú s'hi ha subscrit, invocar-lo directament llançaria una excepció; per això és habitual comprovar-ho abans amb ?.Invoke(...), com veuràs a l'apartat 7.

  1. Esdeveniments: event i el patró publicador/subscriptor

Un delegat públic a seques té un inconvenient: qualsevol codi extern podria no només subscriure's, sinó també reemplaçar completament la llista de subscriptors (amb = en lloc de +=) o fins i tot invocar-lo directament des de fora, trencant la intenció que només el propi objecte decideixi quan "avisar". La paraula clau event resol això: declara un delegat amb regles més estrictes, pensades exactament per al patró publicador/subscriptor.

class Biblioteca
{
    public event Action<Prestec> PrestecRegistrat;
}
  • La classe Biblioteca és la publicadora: és l'única que pot invocar l'esdeveniment (PrestecRegistrat?.Invoke(...)), sempre des de dins de la mateixa classe.
  • Qualsevol altre codi pot ser subscriptor: pot afegir (+=) o treure (-=) un mètode que reaccioni a l'esdeveniment, però no el pot invocar directament ni reemplaçar tota la llista de subscriptors amb =.
void NotificarPrestec(Prestec prestec)
{
    Console.WriteLine($"S'ha prestat '{prestec.Llibre.Titol}' a {prestec.Soci.Nom}");
}

Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestecRegistrat += NotificarPrestec; // subscripcio valida des de fora de la classe

// biblioteca.PrestecRegistrat(prestec1);          // Error de compilacio: no es pot invocar un event des de fora
// biblioteca.PrestecRegistrat = NotificarPrestec;  // Error de compilacio: no es pot reemplacar amb "="

  1. event enfront d'un delegat públic a seques

public Action<Prestec> Camp; public event Action<Prestec> Esdeveniment;
Subscriure amb += des de fora
Reemplaçar tota la llista amb = des de fora Sí (risc d'esborrar altres subscriptors per error) No, error de compilació
Invocar directament des de fora de la classe No, error de compilació
Intenció que comunica en llegir el codi Ambigua: sembla un camp qualsevol Clara: "això és un avís al qual et pots subscriure"

A la pràctica, sempre que el propòsit sigui "avisar que ha passat alguna cosa" (un préstec registrat, una devolució, un material afegit al catàleg), event és l'elecció correcta; un delegat públic sense event rarament es justifica fora de casos molt concrets.

  1. Aplicant esdeveniments a BiblioTech: PrestecRegistrat

Amb el que has après, aquesta és la primera versió de la classe Biblioteca (una versió mínima, centrada només en l'esdeveniment; a la propera lliçó, de Col·leccions, s'ampliarà amb el catàleg complet de materials i socis):

class Biblioteca
{
    public event Action<Prestec> PrestecRegistrat;

    public void RegistrarPrestec(Prestec prestec)
    {
        Console.WriteLine($"Registrant prestec de '{prestec.Llibre.Titol}'...");
        PrestecRegistrat?.Invoke(prestec); // el "?." evita una excepcio si ningu s'hi ha subscrit
    }
}

Un subscriptor típic, pensat com un petit "registre de notificacions":

void RegistreDeNotificacions(Prestec prestec)
{
    Console.WriteLine($"S'ha prestat '{prestec.Llibre.Titol}' a {prestec.Soci.Nom}");
}

I el programa que connecta totes dues peces:

Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestecRegistrat += RegistreDeNotificacions;

Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Soci soci1 = new Soci(1, "Anna Martinez");

llibre1.Prestar();
Prestec prestec1 = new Prestec(llibre1, soci1);
biblioteca.RegistrarPrestec(prestec1);

Sortida:

Registrant prestec de 'Rayuela'...
S'ha prestat 'Rayuela' a Anna Martinez

RegistrarPrestec no coneix RegistreDeNotificacions ni en sap res; simplement invoca el seu esdeveniment, i qui s'hi hagi subscrit (un, diversos, o cap) hi reacciona. Es podria afegir demà un segon subscriptor —per exemple, un que al Mòdul 5 escrigui el préstec en un arxiu— sense tocar ni una línia de Biblioteca ni de RegistrarPrestec.

Errors Comuns i Consells

  • Invocar un esdeveniment sense comprovar null: si encara ningú s'hi ha subscrit, PrestecRegistrat.Invoke(...) (sense ?.) llança una NullReferenceException; fes servir sempre PrestecRegistrat?.Invoke(...) dins de la classe publicadora.
  • Intentar invocar un esdeveniment des de fora de la classe: biblioteca.PrestecRegistrat(p) no compila; només la mateixa classe Biblioteca pot invocar el seu esdeveniment. Si necessites que alguna cosa externa dispari la lògica, exposa un mètode públic (com RegistrarPrestec) que internament invoqui l'esdeveniment.
  • Oblidar desubscriure's (-=) quan ja no cal: si un objecte se subscriu a un esdeveniment d'un altre objecte de vida més llarga i mai es desubscriu, el publicador manté una referència al subscriptor indefinidament, la qual cosa pot impedir que s'alliberi memòria (una introducció a aquest tema; es retoma amb més detall al Mòdul 6, Gestió de Memòria i GC).
  • Confondre l'ordre d'execució de diversos subscriptors amb garanties fortes: encara que a la pràctica s'executen en l'ordre en què es van subscriure amb +=, no convé dissenyar lògica que depengui críticament d'aquest ordre entre subscriptors independents.
  • Consell: si un mètode només necessita informar d'un canvi, sense esperar cap valor de tornada útil, event Action<T> (o EventHandler<T> en API més tradicionals de .NET) és gairebé sempre l'elecció correcta.

Exercicis

  1. Declara un delegat Func<MaterialBibliotecari, bool> anomenat filtre que apunti a un mètode EsLlibreDisponible(MaterialBibliotecari material) (retorna true si el material és un Llibre i està Disponible). Invoca'l sobre dos materials diferents i mostra el resultat.

  2. Crea una classe Biblioteca amb un event Action<Prestec> PrestecRegistrat i un mètode RegistrarPrestec(Prestec prestec) que l'invoqui. Subscriu dos mètodes diferents a l'esdeveniment (un que mostri un missatge de confirmació, un altre que mostri el nom del soci) i comprova que tots dos s'executen en registrar un préstec.

  3. Afegeix a la solució de l'exercici anterior un tercer subscriptor, i després elimina'l amb -= abans de registrar un segon préstec. Comprova que, en el segon préstec, aquest tercer subscriptor ja no s'executa.

Solucions

bool EsLlibreDisponible(MaterialBibliotecari material)
{
    return material is Llibre && material.Disponible;
}

Func<MaterialBibliotecari, bool> filtre = EsLlibreDisponible;

Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);

Console.WriteLine(filtre(llibre1));   // True
Console.WriteLine(filtre(revista1)); // False: no es un Llibre
class Biblioteca
{
    public event Action<Prestec> PrestecRegistrat;

    public void RegistrarPrestec(Prestec prestec)
    {
        PrestecRegistrat?.Invoke(prestec);
    }
}

void Confirmacio(Prestec p) => Console.WriteLine($"Prestec de '{p.Llibre.Titol}' confirmat.");
void MostrarSoci(Prestec p) => Console.WriteLine($"Soci: {p.Soci.Nom}");

Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestecRegistrat += Confirmacio;
biblioteca.PrestecRegistrat += MostrarSoci;

Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Soci soci1 = new Soci(1, "Anna Martinez");
llibre1.Prestar();

biblioteca.RegistrarPrestec(new Prestec(llibre1, soci1));
// Prestec de 'Rayuela' confirmat.
// Soci: Anna Martinez
void AvisTemporal(Prestec p) => Console.WriteLine("Avis temporal de prova");

biblioteca.PrestecRegistrat += AvisTemporal;
// ... s'executarien els tres subscriptors en el seguent prestec ...

biblioteca.PrestecRegistrat -= AvisTemporal; // es retira abans del segon prestec

Llibre llibre2 = new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1");
Soci soci2 = new Soci(2, "Lluis Gomez");
llibre2.Prestar();

biblioteca.RegistrarPrestec(new Prestec(llibre2, soci2));
// Nomes s'executen Confirmacio i MostrarSoci; AvisTemporal ja no s'executa.

Conclusió

En aquesta lliçó has après què és un delegat, com fer servir els predefinits Action, Func i Predicate, com un mateix delegat pot tenir diversos mètodes subscrits alhora (multidifusió), i com event restringeix aquesta capacitat al patró publicador/subscriptor, evitant invocacions o reemplaçaments indeguts des de fora de la classe. Ha nascut la classe Biblioteca, amb el seu esdeveniment PrestecRegistrat, que a les properes lliçons d'aquest mòdul creixerà fins a convertir-se en el punt central de tot el catàleg de BiblioTech.

Els delegats i esdeveniments resolen "com reaccionar a alguna cosa que ja ha passat". La propera lliçó canvia de tema cap a una altra part fonamental del C# modern: el pattern matching, que et permetrà expressar de forma molt més compacta i llegible les comprovacions de tipus (is, as) que ja vas fer servir a la lliçó de Polimorfisme, juntament amb les switch expressions i els nullable reference types.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats