A la lliçó anterior vas aconseguir que Llibre i Revista compartissin, a través de
MaterialBibliotecari, les propietats i mètodes comuns a qualsevol material de la biblioteca.
Però de moment els continues tractant per separat: si volguessis mostrar la descripció de tots
els materials del catàleg, necessitaries dos bucles diferents —un per a llibres i un altre per
a revistes— o una sèrie de comprovacions manuals de tipus. El polimorfisme (del grec
"moltes formes") és la capacitat de tractar objectes de diferents classes heretades d'una
mateixa base de manera uniforme, deixant que cadascun es comporti segons el seu propi tipus
real en temps d'execució. En aquesta lliçó aprendràs a recórrer tot el catàleg de BiblioTech
—llibres i revistes barrejats— amb un únic bucle, i cada material "sabrà" descriure's a si
mateix correctament.
Contingut
- Dos tipus de polimorfisme: recordatori del ja vist
- Polimorfisme en temps d'execució: el mètode
Descriure() - Enllaç dinàmic: qui decideix quina versió s'executa
- Un catàleg mixt:
MaterialBibliotecari[]recorregut polimòrficament - L'operador
is: comprovar el tipus real d'un objecte - L'operador
as: convertir de forma segura - Aplicació: filtrar el catàleg per tipus concret
- Dos tipus de polimorfisme: recordatori del ja vist
En C# es parla de dues formes de polimorfisme:
| Tipus de polimorfisme | Quan es decideix quin codi s'executa | On l'has vist |
|---|---|---|
| En temps de compilació (estàtic) | El compilador, segons el nombre/tipus d'arguments | Sobrecàrrega de mètodes, lliçó "Mètodes" |
| En temps d'execució (dinàmic) | El programa, en executar-se, segons el tipus real de l'objecte | virtual/override, tema d'aquesta lliçó |
Ja vas fer servir el primer sense anomenar-lo pel seu nom: quan vas definir MostrarFitxa() i
MostrarFitxa(bool incloureIsbn) a la lliçó de Mètodes, el compilador decidia, en el moment de
compilar el programa, quina de les dues versions invocar, segons quants arguments li
passessis. Aquesta lliçó se centra en el segon tipus, molt més característic de la
programació orientada a objectes.
- Polimorfisme en temps d'execució: el mètode
Descriure()
Descriure()A la lliçó anterior, MostrarFitxa() ja era virtual a MaterialBibliotecari i es
sobreescrivia a Llibre i Revista. Afegirem un segon mètode virtual, Descriure(), pensat
específicament per il·lustrar el polimorfisme en acció: en lloc d'imprimir directament per
consola, retorna un string amb una descripció breu del material, diferent segons el seu
tipus concret.
class MaterialBibliotecari
{
public string Titol { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
public MaterialBibliotecari(string titol, string autor)
{
Titol = titol;
Autor = autor;
}
public virtual string Descriure()
{
return $"Material: {Titol}, de {Autor}";
}
}
class Llibre : MaterialBibliotecari
{
public string Isbn { get; set; }
public Llibre(string titol, string autor, string isbn) : base(titol, autor)
{
Isbn = isbn;
}
public override string Descriure()
{
return $"Llibre: {Titol}, de {Autor} (ISBN {Isbn})";
}
}
class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
public override string Descriure()
{
return $"Revista: {Titol}, edicio numero {NumeroEdicio}";
}
}Cada classe ofereix la seva pròpia versió de Descriure(), adaptada al que té sentit mostrar
per a aquest tipus concret de material.
- Enllaç dinàmic: qui decideix quina versió s'executa
Ara ve la part clau del polimorfisme. Observa aquest codi:
MaterialBibliotecari material = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(material.Descriure());
// Llibre: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4)La variable material està declarada com a tipus MaterialBibliotecari, però l'objecte a què
apunta és, en realitat, un Llibre. En cridar material.Descriure(), C# no executa la versió
de MaterialBibliotecari (que seria el previsible si només es mirés el tipus de la variable),
sinó la versió sobreescrita a Llibre, perquè aquest és el tipus real de l'objecte en
memòria. Aquest mecanisme s'anomena enllaç dinàmic (dynamic binding o late binding):
quin codi s'executa es decideix en temps d'execució, segons el tipus real de l'objecte, no
segons el tipus declarat de la variable que el referencia.
Això només passa perquè Descriure() és virtual a la classe base i està override a la
hereva; si Descriure() no fos virtual, material.Descriure() executaria sempre la versió
de MaterialBibliotecari, independentment de quin objecte concret contingués realment la
variable.
- Un catàleg mixt:
MaterialBibliotecari[] recorregut polimòrficament
MaterialBibliotecari[] recorregut polimòrficamentAquí és on el polimorfisme demostra la seva veritable utilitat: un array (o qualsevol col·lecció) declarat amb el tipus base pot contenir objectes de qualsevol de les seves classes hereves, barrejats, i recórrer-se amb un únic bucle:
MaterialBibliotecari[] cataleg = new MaterialBibliotecari[]
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Diversos autors", 302),
new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1")
};
foreach (MaterialBibliotecari material in cataleg)
{
Console.WriteLine(material.Descriure());
}Sortida:
Llibre: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4) Revista: National Geographic, edicio numero 302 Llibre: Ficcions, de Jorge Luis Borges (ISBN 978-84-376-0496-1)
Un únic bucle foreach, escrit una sola vegada sobre el tipus base MaterialBibliotecari,
produeix automàticament la descripció correcta per a cada element, sigui Llibre o Revista,
sense que el bucle necessiti saber res sobre aquests tipus concrets ni contenir cap
comprovació explícita de tipus. Afegir demà una tercera classe hereva (per exemple, un futur
Dvd : MaterialBibliotecari) no requeriria tocar aquest bucle en absolut: bastaria que la nova
classe sobreescrivís Descriure() a la seva manera. Aquesta és l'essència del polimorfisme: el
codi que recorre el catàleg roman estable encara que el catàleg creixi en tipus diferents.
- L'operador
is: comprovar el tipus real d'un objecte
is: comprovar el tipus real d'un objecteDe vegades, però, necessites fer alguna cosa específica d'un tipus concret, no només allò que
ofereix la classe base. L'operador is comprova si un objecte és (o deriva d') un tipus
determinat, i opcionalment l'assigna directament a una variable d'aquest tipus més específic:
foreach (MaterialBibliotecari material in cataleg)
{
if (material is Llibre llibre)
{
Console.WriteLine($"Es un llibre; el seu ISBN es {llibre.Isbn}");
}
else if (material is Revista revista)
{
Console.WriteLine($"Es una revista; el seu numero d'edicio es {revista.NumeroEdicio}");
}
}material is Llibre llibre es llegeix com "si material és (en temps d'execució) un
Llibre, aleshores assigna aquesta mateixa referència, ja amb el tipus Llibre, a una nova
variable anomenada llibre". Dins del bloc if, llibre ja dona accés a Isbn, propietat
que no existeix a MaterialBibliotecari i que, per tant, no seria accessible directament a
través de la variable material. Aquest patró s'anomena pattern matching amb is, i
s'ampliarà força al Mòdul 4 ("Pattern Matching i Característiques Modernes").
- L'operador
as: convertir de forma segura
as: convertir de forma seguraL'operador as intenta convertir (castejar) un objecte a un tipus més específic, i retorna
null si la conversió no és possible, en lloc de llançar una excepció:
MaterialBibliotecari primerElement = cataleg[0];
Llibre? llibreConvertit = primerElement as Llibre;
if (llibreConvertit != null)
{
Console.WriteLine($"Conversio correcta: {llibreConvertit.Isbn}");
}
else
{
Console.WriteLine("L'element no era un Llibre.");
}Si primerElement fos en realitat una Revista, primerElement as Llibre no llançaria cap
error: simplement llibreConvertit valdria null, i el codi pot comprovar-ho amb
tranquil·litat. Això contrasta amb una conversió directa entre parèntesis, com (Llibre) primerElement, que si l'objecte no és realment un Llibre, sí que llança una excepció
(InvalidCastException) en temps d'execució. A la pràctica moderna de C#, is amb patró
(apartat anterior) se sol preferir a as per a aquest tipus de comprovacions, per ser més
compacte i llegible, però convé conèixer tots dos.
| Operador/tècnica | Si el tipus no coincideix | Quan usar-lo |
|---|---|---|
(Llibre) material (conversió directa) |
Llança InvalidCastException |
Quan estàs segur del tipus i un error seria un senyal real de fallada |
material as Llibre |
Retorna null |
Quan la conversió pot fallar raonablement i prefereixes comprovar null |
material is Llibre llibre |
La condició de l'if és false (no entra al bloc) |
La forma més llegible en C# modern; combina comprovació i assignació en un pas |
- Aplicació: filtrar el catàleg per tipus concret
Tanquem la lliçó combinant el recorregut polimòrfic amb is, per comptar quants elements del
catàleg són llibres i quants són revistes —una operació habitual en generar estadístiques del
catàleg de BiblioTech:
MaterialBibliotecari[] cataleg = new MaterialBibliotecari[]
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Diversos autors", 302),
new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"),
new Revista("Muy Interesante", "Diversos autors", 487)
};
int totalLlibres = 0;
int totalRevistes = 0;
foreach (MaterialBibliotecari material in cataleg)
{
Console.WriteLine(material.Descriure()); // polimorfisme: cadascu es descriu a la seva manera
if (material is Llibre)
{
totalLlibres++;
}
else if (material is Revista)
{
totalRevistes++;
}
}
Console.WriteLine($"Total de llibres: {totalLlibres}");
Console.WriteLine($"Total de revistes: {totalRevistes}");Nota que a if (material is Llibre), sense declarar cap variable addicional, n'hi ha prou per
preguntar "és aquest objecte un Llibre?" sense necessitat d'accedir a cap de les seves
propietats específiques —útil quan només interessa comptar o classificar, no operar amb les
dades pròpies del tipus concret.
Errors Comuns i Consells
- Oblidar
virtuala la classe base: siDescriure()no ésvirtual, escriureoverrideaLlibreoRevistaprovoca un error de compilació; i si en el seu lloc fas servir la paraula claunewper "amagar" el mètode (en lloc de sobreescriure'l), perdràs el polimorfisme:material.Descriure()tornaria a executar sempre la versió deMaterialBibliotecari, encara que l'objecte real fos unLlibre. - Castejar directament sense comprovar el tipus:
(Llibre) materialsense verificar abans ambispot llançarInvalidCastExceptionsi l'objecte real no és unLlibre; prefereixisamb patró oasquan no estiguis segur del tipus real. - Escriure un bucle diferent per a cada tipus hereu: si et sorprens escrivint un
foreachper a llibres i un altre gairebé idèntic per a revistes, és un senyal que hauries d'estar recorrent el tipus base (MaterialBibliotecari[]) amb un únic bucle polimòrfic, com a l'apartat 4. - Confondre el tipus de la variable amb el tipus real de l'objecte: una variable declarada
com a
MaterialBibliotecaripot apuntar a unLlibreo a unaRevista; el que determina quina versió d'un mètodevirtuals'executa és sempre el tipus real de l'objecte, mai el tipus amb què es va declarar la variable. - Consell: abans de recórrer a
is/asper distingir tipus dins d'un bucle, pregunta't si aquest comportament diferent no es podria resoldre simplement sobreescrivint un mètodevirtuala cada classe hereva; sol ser un disseny més net i extensible.
Exercicis
-
Sobre les classes
MaterialBibliotecari,LlibreiRevistadefinides en aquesta lliçó (ambDescriure()com a mètodevirtual/override), crea un arrayMaterialBibliotecari[]amb dos llibres i una revista, i recorre'l ambforeachmostrant el resultat deDescriure()de cadascun. -
Sobre el mateix array de l'exercici anterior, fes servir l'operador
isamb patró per mostrar, només per als elements que siguinLlibre, un missatge amb el seu ISBN (per exemple:"ISBN: 978-84-376-0495-4"). -
Escriu una expressió amb l'operador
asque intenti convertir el primer element de l'array aRevista, i mostri"Es una revista amb edicio <numero>"si la conversió té èxit, o"No es una revista"si el resultat ésnull.
Solucions
MaterialBibliotecari[] cataleg = new MaterialBibliotecari[]
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"),
new Revista("National Geographic", "Diversos autors", 302)
};
foreach (MaterialBibliotecari material in cataleg)
{
Console.WriteLine(material.Descriure());
}
foreach (MaterialBibliotecari material in cataleg)
{
if (material is Llibre llibre)
{
Console.WriteLine($"ISBN: {llibre.Isbn}");
}
}
Revista? revistaConvertida = cataleg[0] as Revista;
if (revistaConvertida != null)
{
Console.WriteLine($"Es una revista amb edicio {revistaConvertida.NumeroEdicio}");
}
else
{
Console.WriteLine("No es una revista");
}
Com que cataleg[0] és en realitat un Llibre (segons l'exercici 1), la conversió amb as
retorna null, i el resultat mostrat és "No es una revista".
Conclusió
En aquesta lliçó has vist el polimorfisme en temps d'execució en acció: un mètode virtual
com Descriure(), sobreescrit de forma diferent a Llibre i Revista, permet recórrer un
catàleg mixt (MaterialBibliotecari[]) amb un únic bucle i obtenir el comportament correcte
per a cada objecte segons el seu tipus real —gràcies a l'enllaç dinàmic. També has après a fer
servir is i as per accedir puntualment a les particularitats d'un tipus concret quan de
veritat ho necessites, sense renunciar al polimorfisme com a estratègia principal.
Fins ara, però, Disponible es pot modificar lliurement des de fora de la classe
(material.Disponible = true;, sense passar per Prestar() ni Retornar()), la qual cosa
podria deixar el catàleg de BiblioTech en un estat incoherent per error o descuit. A la propera
lliçó, Encapsulació, aprendràs a protegir l'estat intern de les teves classes, permetent
que només es modifiqui a través dels mètodes pensats per a això.
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
