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

  1. El problema sense genèrics: duplicació de codi o object i boxing
  2. Classes genèriques: Repositori<T>
  3. Mètodes genèrics
  4. Restriccions: where T : ...
  5. Especialitzant Repositori<T> per a BiblioTech
  6. Genèrics amb diversos paràmetres de tipus (avançament de Dictionary<TKey, TValue>)

  1. El problema sense genèrics: duplicació de codi o object i boxing

Sense 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 explicita

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

  1. Classes genèriques: 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);   // 1

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

  1. 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); // -1

Fixa'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.

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

  1. Especialitzant Repositori<T> per a BiblioTech

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

  1. Genèrics amb diversos paràmetres de tipus (avançament de 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 de Repositori<T> no es pot escriure element.Titol, perquè el compilador no sap que T tindrà 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> i T 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 nom T, 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 preferir object.
  • Oblidar que default pot ser sorprenent per a tipus per valor: default(int) és 0, default(bool) és false; si un mètode genèric retorna default per indicar "no trobat" i T és int, un resultat de 0 es podria confondre amb un valor real trobat. Amb tipus per referència (com MaterialBibliotecari), default sempre és null, més fàcil de distingir amb is 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

  1. Defineix class Repositori<T> amb una llista interna List<T>, un mètode Afegir(T element) i una propietat Quantitat. Crea un Repositori<Llibre> i un Repositori<Soci>, afegeix-hi dos elements a cadascun i mostra la quantitat de cada repositori.

  2. Afegeix a Repositori<T> un mètode T? Cercar(Func<T, bool> criteri) que retorni el primer element que compleixi el criteri, o default si cap el compleix. Cerca'l per títol en un Repositori<Llibre> i per Id en un Repositori<Soci>.

  3. Defineix class RepositoriPrestable<T> where T : IPrestable amb un mètode List<T> Disponibles() que retorni únicament els elements amb Disponible == true. Afegeix dos llibres (un prestat, un altre disponible) i comprova que Disponibles() 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#

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