Tots els exemples de BiblioTech vistos fins ara han treballat amb tipus concrets: un mètode que
rep un Llibre, una llista pensada només per a MaterialBibliotecari. Però, què passa quan
necessites la mateixa lògica —guardar elements, cercar-los, comptar-los— per a Soci, per a
Prestec, o per a qualsevol tipus futur que ni tan sols existeix encara? Escriure una classe
diferent per a cada tipus seria repetir el mateix codi una vegada i una altra. Els genèrics
resolen exactament aquest problema: permeten escriure una classe o un mètode una única vegada,
deixant el tipus concret amb què treballarà com un paràmetre més, que es decideix en fer-lo
servir. Aquesta lliçó construeix un Repositori<T> reutilitzable i l'especialitza per a
materials i socis de BiblioTech, posant la base sobre la qual es construirà la classe
Biblioteca a la propera lliçó.
Contingut
- El problema sense genèrics: duplicació de codi o
objecti boxing - Classes genèriques:
Repositori<T> - Mètodes genèrics
- Restriccions:
where T : ... - Especialitzant
Repositori<T>per a BiblioTech - Genèrics amb diversos paràmetres de tipus (avançament de
Dictionary<TKey, TValue>)
- El problema sense genèrics: duplicació de codi o
object i boxing
object i boxingSense genèrics, hi ha dues maneres habituals d'escriure una classe reutilitzable per a "qualsevol tipus", i cap és satisfactòria. La primera és duplicar la classe sencera canviant el tipus:
class RepositoriDeLlibres
{
private List<Llibre> _elements = new List<Llibre>();
public void Afegir(Llibre element) => _elements.Add(element);
}
class RepositoriDeSocis
{
private List<Soci> _elements = new List<Soci>();
public void Afegir(Soci element) => _elements.Add(element);
}El codi és idèntic llevat del tipus: qualsevol correcció o millora s'ha de repetir a cada còpia.
La segona alternativa, anterior als genèrics en versions molt antigues de .NET, era fer servir
object com a tipus universal:
class RepositoriObjectes
{
private List<object> _elements = new List<object>();
public void Afegir(object element) => _elements.Add(element);
}Això sí que és reutilitzable, però té dos problemes seriosos: es perd tota comprovació de tipus
en temps de compilació (res impedeix afegir un Soci a un repositori pensat per a Llibre, i
l'error només apareixeria en temps d'execució en intentar convertir de nou), i, per a tipus per
valor com int o DateTime, guardar-los com a object obliga a un procés anomenat boxing:
embolicar el valor en un objecte de memòria dinàmica (i unboxing en recuperar-lo), amb un
cost de rendiment mesurable que es repeteix a cada operació.
List<object> numeros = new List<object>();
numeros.Add(42); // boxing: l'int 42 s'embolica en un object al heap
int valor = (int)numeros[0]; // unboxing: es recupera, amb conversio explicitaEls genèrics resolen tots dos problemes alhora: seguretat de tipus en temps de compilació, sense duplicar codi, i sense boxing per a tipus per valor.
- Classes genèriques:
Repositori<T>
Repositori<T>Una classe genèrica declara un o més paràmetres de tipus entre < i > al costat del seu
nom (per convenció, una única lletra majúscula com T, de Type), i els fa servir com si fossin
un tipus normal dins de la classe:
class Repositori<T>
{
private List<T> _elements = new List<T>();
public void Afegir(T element)
{
_elements.Add(element);
}
public int Quantitat => _elements.Count;
}T no és cap tipus concret: és un marcador de posició que el compilador substitueix pel tipus
real en el moment de fer servir la classe, indicat entre <>:
Repositori<Llibre> repositoriLlibres = new Repositori<Llibre>();
repositoriLlibres.Afegir(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
// repositoriLlibres.Afegir(new Soci(1, "Anna Martinez")); // Error de compilacio: Soci no es Llibre
Repositori<Soci> repositoriSocis = new Repositori<Soci>();
repositoriSocis.Afegir(new Soci(1, "Anna Martinez"));
Console.WriteLine(repositoriLlibres.Quantitat); // 1
Console.WriteLine(repositoriSocis.Quantitat); // 1Amb una única definició de Repositori<T>, el compilador genera (conceptualment) una versió
especialitzada per a cada tipus amb què es faci servir, amb comprovació de tipus completa:
l'error d'intentar afegir un Soci a Repositori<Llibre> es detecta en compilar, no en
executar.
Un mètode Cercar completa la classe, acceptant un criteri de cerca com a paràmetre (fent
servir Func<T, bool>, ja conegut de la lliçó de Delegats i Esdeveniments):
class Repositori<T>
{
private List<T> _elements = new List<T>();
public void Afegir(T element)
{
_elements.Add(element);
}
public T? Cercar(Func<T, bool> criteri)
{
foreach (T element in _elements)
{
if (criteri(element))
{
return element;
}
}
return default; // default(T): null per a tipus per referencia, 0/false/etc. per a tipus per valor
}
public int Quantitat => _elements.Count;
}default (equivalent a default(T)) retorna el valor per defecte de T, sigui quin sigui:
null si T és un tipus per referència (com Llibre), o el valor per defecte corresponent
(0, false, una data mínima...) si T és un tipus per valor. Això és quelcom que només un
genèric pot escriure sense saber d'entrada quin tipus és T.
- Mètodes genèrics
A més de classes genèriques completes, un únic mètode (dins d'una classe normal o genèrica) pot declarar el seu propi paràmetre de tipus, independent del de la classe que el conté:
class Utilitats
{
public static T PrimerODefecte<T>(List<T> llista, T valorPerDefecte)
{
return llista.Count > 0 ? llista[0] : valorPerDefecte;
}
}List<string> noms = new List<string> { "Anna", "Lluis" };
string primer = Utilitats.PrimerODefecte(noms, "Sense nom"); // "Anna"
List<int> buits = new List<int>();
int primerNumero = Utilitats.PrimerODefecte(buits, -1); // -1Fixa't que no ha calgut escriure Utilitats.PrimerODefecte<string>(...) explícitament: el
compilador infereix el tipus T a partir dels arguments (noms és List<string>, així que
T és string). Escriure el tipus explícitament entre <> continua sent vàlid i de vegades
necessari, però rarament cal.
- Restriccions:
where T : ...
where T : ...Sense cap restricció, dins d'una classe o mètode genèric només es pot assumir que T és
"qualsevol tipus", cosa que limita molt el que s'hi pot fer (ni tan sols es podria cridar
element.Titol, perquè no tot tipus té aquesta propietat). Una restricció (where T : ...)
redueix el conjunt de tipus acceptats a canvi de poder assumir més sobre ells dins de la classe:
class RepositoriPrestable<T> where T : IPrestable
{
private List<T> _elements = new List<T>();
public void Afegir(T element)
{
_elements.Add(element);
}
public List<T> Disponibles()
{
List<T> resultat = new List<T>();
foreach (T element in _elements)
{
if (element.Disponible) // valid: T esta restringit a IPrestable, que declara Disponible
{
resultat.Add(element);
}
}
return resultat;
}
}Gràcies a where T : IPrestable (recordant la interfície de la primera lliçó d'aquest mòdul),
el compilador sap que qualsevol T amb què es faci servir aquesta classe tindrà, com a mínim,
Disponible, Prestar() i Retornar(), i permet fer-los servir dins de la classe genèrica
sense cap conversió addicional.
| Restricció | Significat |
|---|---|
where T : MaterialBibliotecari |
T ha de ser MaterialBibliotecari o una classe hereva |
where T : IPrestable |
T ha d'implementar la interfície IPrestable |
where T : class |
T ha de ser un tipus per referència (no un struct) |
where T : struct |
T ha de ser un tipus per valor |
where T : new() |
T ha de tenir un constructor públic sense paràmetres, permetent new T() dins de la classe genèrica |
| Diverses alhora | where T : MaterialBibliotecari, ICercable — s'han de complir totes |
- Especialitzant
Repositori<T> per a BiblioTech
Repositori<T> per a BiblioTechAmb Repositori<T> ja definit, s'especialitza per als dos tipus centrals del model de
BiblioTech:
Repositori<MaterialBibliotecari> repositoriMaterials = new Repositori<MaterialBibliotecari>();
repositoriMaterials.Afegir(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
repositoriMaterials.Afegir(new Revista("National Geographic", "Varios autores", 302));
MaterialBibliotecari? trobat = repositoriMaterials.Cercar(m => m.Titol == "Rayuela");
if (trobat is Llibre llibreTrobat)
{
Console.WriteLine($"Trobat: {llibreTrobat.Isbn}");
}
Repositori<Soci> repositoriSocis = new Repositori<Soci>();
repositoriSocis.Afegir(new Soci(1, "Anna Martinez"));
repositoriSocis.Afegir(new Soci(2, "Lluis Gomez"));
Soci? sociTrobat = repositoriSocis.Cercar(s => s.Id == 2);
Console.WriteLine(sociTrobat?.Nom); // "Lluis Gomez"Repositori<MaterialBibliotecari> i Repositori<Soci> comparteixen exactament el mateix codi
d'Afegir, Cercar i Quantitat, escrit una única vegada. Aquest mateix Repositori<T> podria,
en principi, donar suport internament al catàleg de la classe Biblioteca; a la propera lliçó,
en canvi, Biblioteca es construirà directament sobre List<T> i Dictionary<TKey, TValue>
(les col·leccions estàndard de .NET), que és l'enfocament més habitual en codi real quan no cal
la lògica afegida d'un repositori propi. Tots dos enfocaments són vàlids; el que importa
d'aquest apartat és veure els genèrics aplicats a un cas complet.
- Genèrics amb diversos paràmetres de tipus (avançament de
Dictionary<TKey, TValue>)
Dictionary<TKey, TValue>)Una classe o mètode genèric pot declarar més d'un paràmetre de tipus alhora, separats per comes.
L'exemple més habitual, que faràs servir a la propera lliçó, és Dictionary<TKey, TValue>, amb
dos paràmetres de tipus independents: el tipus de la clau i el tipus del valor associat.
class Parell<TClau, TValor>
{
public TClau Clau { get; }
public TValor Valor { get; }
public Parell(TClau clau, TValor valor)
{
Clau = clau;
Valor = valor;
}
}
Parell<int, Soci> parellSoci = new Parell<int, Soci>(1, new Soci(1, "Anna Martinez"));
Console.WriteLine($"{parellSoci.Clau} -> {parellSoci.Valor.Nom}"); // "1 -> Anna Martinez"Aquesta idea —dos paràmetres de tipus independents, un per a la clau i un altre per al valor— és
exactament la que fa servir Dictionary<TKey, TValue>, que estudiaràs en profunditat a la
propera lliçó per indexar els socis de BiblioTech pel seu Id.
Errors Comuns i Consells
- Oblidar
where T : ...i esperar poder fer servir membres d'un tipus concret: sense restricció, dins deRepositori<T>no es pot escriureelement.Titol, perquè el compilador no sap queTtindrà aquesta propietat; afegeix la restricció necessària (where T : MaterialBibliotecari, o una interfície) per poder accedir a aquests membres. - Confondre el paràmetre de tipus de la classe amb el d'un mètode:
class Repositori<T>iT Metode<T>(...)dins d'una classe no genèrica són coses diferents; un mètode genèric dins d'una classe genèrica pot fins i tot reutilitzar el nomT, encara que convé evitar-ho per no confondre la lectura. - Fer servir
object"per simplicitat" en lloc d'un genèric: renuncia a la comprovació de tipus en compilació i, per a tipus per valor, introdueix boxing innecessari; amb genèrics disponibles des de les primeres versions modernes de C#, rarament hi ha una bona raó per preferirobject. - Oblidar que
defaultpot ser sorprenent per a tipus per valor:default(int)és0,default(bool)ésfalse; si un mètode genèric retornadefaultper indicar "no trobat" iTésint, un resultat de0es podria confondre amb un valor real trobat. Amb tipus per referència (comMaterialBibliotecari),defaultsempre ésnull, més fàcil de distingir ambis null. - Consell: anomena els paràmetres de tipus amb una única lletra quan el propòsit és genèric i
evident pel context (
T,TKey,TValue), i amb un nom més descriptiu (TMaterial,TSoci) només si aporta claredat real en una classe amb diversos paràmetres de tipus relacionats entre si.
Exercicis
-
Defineix
class Repositori<T>amb una llista internaList<T>, un mètodeAfegir(T element)i una propietatQuantitat. Crea unRepositori<Llibre>i unRepositori<Soci>, afegeix-hi dos elements a cadascun i mostra la quantitat de cada repositori. -
Afegeix a
Repositori<T>un mètodeT? Cercar(Func<T, bool> criteri)que retorni el primer element que compleixi el criteri, odefaultsi cap el compleix. Cerca'l per títol en unRepositori<Llibre>i perIden unRepositori<Soci>. -
Defineix
class RepositoriPrestable<T> where T : IPrestableamb un mètodeList<T> Disponibles()que retorni únicament els elements ambDisponible == true. Afegeix dos llibres (un prestat, un altre disponible) i comprova queDisponibles()retorna només el que està lliure.
Solucions
class Repositori<T>
{
private List<T> _elements = new List<T>();
public void Afegir(T element)
{
_elements.Add(element);
}
public int Quantitat => _elements.Count;
}
Repositori<Llibre> repositoriLlibres = new Repositori<Llibre>();
repositoriLlibres.Afegir(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
repositoriLlibres.Afegir(new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"));
Repositori<Soci> repositoriSocis = new Repositori<Soci>();
repositoriSocis.Afegir(new Soci(1, "Anna Martinez"));
repositoriSocis.Afegir(new Soci(2, "Lluis Gomez"));
Console.WriteLine(repositoriLlibres.Quantitat); // 2
Console.WriteLine(repositoriSocis.Quantitat); // 2
public T? Cercar(Func<T, bool> criteri)
{
foreach (T element in _elements)
{
if (criteri(element))
{
return element;
}
}
return default;
}
Llibre? llibreTrobat = repositoriLlibres.Cercar(l => l.Titol == "Rayuela");
Console.WriteLine(llibreTrobat?.Isbn); // "978-84-376-0495-4"
Soci? sociTrobat = repositoriSocis.Cercar(s => s.Id == 2);
Console.WriteLine(sociTrobat?.Nom); // "Lluis Gomez"
class RepositoriPrestable<T> where T : IPrestable
{
private List<T> _elements = new List<T>();
public void Afegir(T element) => _elements.Add(element);
public List<T> Disponibles()
{
List<T> resultat = new List<T>();
foreach (T element in _elements)
{
if (element.Disponible)
{
resultat.Add(element);
}
}
return resultat;
}
}
RepositoriPrestable<Llibre> repositori = new RepositoriPrestable<Llibre>();
Llibre disponible = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Llibre prestat = new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1");
prestat.Prestar();
repositori.Afegir(disponible);
repositori.Afegir(prestat);
List<Llibre> disponibles = repositori.Disponibles();
Console.WriteLine(disponibles.Count); // 1
Console.WriteLine(disponibles[0].Titol); // "Rayuela"
Conclusió
En aquesta lliçó has après a escriure classes i mètodes genèrics, a restringir un paràmetre de
tipus amb where T : ... per poder fer servir membres concrets dins de la classe genèrica, i a
evitar tant la duplicació de codi com el boxing d'object. Repositori<T> demostra que una
única implementació pot servir, de forma segura, tant per a MaterialBibliotecari com per a
Soci.
Els genèrics són, precisament, la base sobre la qual estan construïdes les col·leccions de .NET
que faràs servir a partir d'ara sense pensar-hi dues vegades: List<T>, que ja coneixes des del
Mòdul 1, i Dictionary<TKey, TValue>, amb dos paràmetres de tipus com el Parell<TClau, TValor> de l'últim apartat. La propera lliçó, Col·leccions, retoma totes dues per construir
finalment la classe Biblioteca completa: un catàleg central de materials i socis, gestionat en
memòria, a punt per consultar-se amb LINQ a la lliçó següent.
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
