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
- El problema: avisar sense acoblar qui avisa amb qui escolta
- Delegats: variables que apunten a mètodes
- Delegats predefinits:
Action,FunciPredicate - Multidifusió: un delegat amb diversos mètodes subscrits
- Esdeveniments:
eventi el patró publicador/subscriptor eventenfront d'un delegat públic a seques- Aplicant esdeveniments a BiblioTech:
PrestecRegistrat
- 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.
- 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:
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 delegatfiltre(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à.
- Delegats predefinits:
Action, Func i Predicate
Action, Func i PredicateDeclarar 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");
- 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.
- Esdeveniments:
event i el patró publicador/subscriptor
event i el patró publicador/subscriptorUn 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.
- 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 "="
event enfront d'un delegat públic a seques
event enfront d'un delegat públic a sequespublic Action<Prestec> Camp; |
public event Action<Prestec> Esdeveniment; |
|
|---|---|---|
Subscriure amb += des de fora |
Sí | Sí |
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 | Sí | 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.
- Aplicant esdeveniments a BiblioTech:
PrestecRegistrat
PrestecRegistratAmb 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:
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 unaNullReferenceException; fes servir semprePrestecRegistrat?.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 classeBibliotecapot invocar el seu esdeveniment. Si necessites que alguna cosa externa dispari la lògica, exposa un mètode públic (comRegistrarPrestec) 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>(oEventHandler<T>en API més tradicionals de .NET) és gairebé sempre l'elecció correcta.
Exercicis
-
Declara un delegat
Func<MaterialBibliotecari, bool>anomenatfiltreque apunti a un mètodeEsLlibreDisponible(MaterialBibliotecari material)(retornatruesi el material és unLlibrei estàDisponible). Invoca'l sobre dos materials diferents i mostra el resultat. -
Crea una classe
Bibliotecaamb unevent Action<Prestec> PrestecRegistrati un mètodeRegistrarPrestec(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. -
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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
