En tancar la lliçó anterior va quedar pendent un problema: Llibre ja té Titol, Autor,
Disponible, Prestar(), Retornar() i MostrarFitxa() perfectament definits, però si
BiblioTech volgués incorporar també revistes al catàleg, hauries de crear una classe Revista
copiant i enganxant exactament aquestes mateixes propietats i mètodes, canviant només allò
específic d'una revista (per exemple, el seu número d'edició). Aquesta duplicació és
precisament el que la herència evita: permet definir una classe base amb allò que diversos
tipus tenen en comú, i que altres classes hereten automàticament aquestes propietats i
mètodes, afegint només allò que les distingeix. En aquesta lliçó extrauràs de Llibre una
classe base, MaterialBibliotecari, de la qual heretaran tant Llibre com una nova classe
Revista.
Contingut
- El problema: propietats i mètodes duplicats
- Heretar amb
:— la classe baseMaterialBibliotecari Llibrehereta deMaterialBibliotecari- L'operador
baseper cridar el constructor de la classe base - Afegint
Revistacom a segona classe hereva - Sobreescriure membres heretats amb
virtualioverride - Classes segellades amb
sealed - Jerarquia d'una sola herència en C#
- El problema: propietats i mètodes duplicats
Recupera la classe Llibre tal com va quedar a la lliçó anterior:
class Llibre
{
public string Titol { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; } = true;
public Llibre(string titol, string autor, string isbn)
{
Titol = titol;
Autor = autor;
Isbn = isbn;
}
public void Prestar() { /* ... */ }
public void Retornar() { /* ... */ }
public void MostrarFitxa() { /* ... */ }
}Si ara escrivissis una classe Revista des de zero, amb Titol, Autor, Disponible,
Prestar(), Retornar() i MostrarFitxa() pràcticament idèntics, i només Isbn substituït
per, per exemple, NumeroEdicio, estaries duplicant una quantitat considerable de codi. I el
que és pitjor: si més endavant corregeixes un error a Prestar(), hauries de recordar
corregir-lo també a la còpia de Revista. L'herència resol aquest problema d'arrel.
- Heretar amb
: — la classe base MaterialBibliotecari
: — la classe base MaterialBibliotecariEl primer pas és identificar què tenen en comú un llibre i una revista dins de BiblioTech:
tots dos tenen un títol, un autor, i poden estar disponibles o prestats. Aquesta part comuna
s'extreu a una nova classe, MaterialBibliotecari, que actuarà com a classe base:
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 void Prestar()
{
if (Disponible)
{
Disponible = false;
Console.WriteLine($"'{Titol}' ha estat prestat.");
}
else
{
Console.WriteLine($"'{Titol}' no esta disponible per a prestec.");
}
}
public void Retornar()
{
Disponible = true;
Console.WriteLine($"'{Titol}' ha estat retornat.");
}
public void MostrarFitxa()
{
Console.WriteLine($"Titol: {Titol}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}MaterialBibliotecari no és un concepte que s'utilitzi "en solitari" a BiblioTech —mai
crearàs un objecte que sigui "un material bibliotecari" a seques, sense més—, sinó la base
sobre la qual es construeixen conceptes més concrets com Llibre o Revista. Aquesta idea es
retomarà a la lliçó d'Abstracció, més endavant en el mòdul.
Llibre hereta de MaterialBibliotecari
Llibre hereta de MaterialBibliotecariPerquè una classe hereti d'una altra, s'escriuen dos punts (:) després del nom de la classe,
seguits del nom de la classe base:
class Llibre : MaterialBibliotecari
{
public string Isbn { get; set; }
public Llibre(string titol, string autor, string isbn) : base(titol, autor)
{
Isbn = isbn;
}
}class Llibre : MaterialBibliotecari significa "Llibre és un MaterialBibliotecari, a
més de tenir les seves pròpies particularitats". Gràcies a l'herència, Llibre obté
automàticament totes les propietats (Titol, Autor, Disponible) i mètodes (Prestar(),
Retornar(), MostrarFitxa()) de MaterialBibliotecari, sense tornar-los a escriure, i només
necessita declarar allò que li és propi: Isbn.
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(llibre1.Titol); // Rayuela (heretat de MaterialBibliotecari)
Console.WriteLine(llibre1.Isbn); // 978-84-376-0495-4 (propi de Llibre)
llibre1.Prestar(); // 'Rayuela' ha estat prestat. (heretat)
llibre1.MostrarFitxa(); // heretat, mostra Titol/Autor/Disponible
- L'operador
base per cridar el constructor de la classe base
base per cridar el constructor de la classe baseAl constructor de Llibre de l'apartat anterior apareix : base(titol, autor) després de la
signatura del constructor. La paraula clau base permet invocar explícitament un membre
de la classe base —en aquest cas, el seu constructor— des de la classe hereva.
Això és necessari perquè MaterialBibliotecari exigeix titol i autor en el seu propi
constructor (no té un constructor sense paràmetres), així que qualsevol classe que hereti
d'ella ha d'indicar explícitament com es construeix aquesta part heretada. L'ordre d'execució
és sempre: primer s'executa el constructor de la classe base
(MaterialBibliotecari(titol, autor), que assigna Titol i Autor), i després el cos del
constructor de Llibre (que assigna Isbn).
base també es pot fer servir dins d'un mètode per invocar la versió de la classe base
d'aquest mateix mètode (ho veuràs a l'apartat 6, amb MostrarFitxa()), no només en
constructors.
- Afegint
Revista com a segona classe hereva
Revista com a segona classe herevaAmb MaterialBibliotecari ja definida, afegir Revista al catàleg de BiblioTech és ara molt
més senzill: només cal allò que la distingeix d'un llibre, que en aquest cas serà el seu
número d'edició.
class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
}Revista revista1 = new Revista("National Geographic", "Diversos autors", 302);
revista1.MostrarFitxa(); // heretat: Titol, Autor, Disponible
Console.WriteLine(revista1.NumeroEdicio); // 302 (propi de Revista)
revista1.Prestar(); // heretat, funciona igual que a LlibreLlibre i Revista comparteixen tota la lògica de Titol, Autor, Disponible, Prestar()
i Retornar() sense haver-la escrita dues vegades: totes dues hereten de la mateixa classe
base MaterialBibliotecari, i cadascuna afegeix únicament la seva particularitat (Isbn en
un cas, NumeroEdicio en l'altre).
- Sobreescriure membres heretats amb
virtual i override
virtual i overridePer defecte, un mètode heretat es comporta exactament igual a la classe hereva que a la classe
base: llibre1.MostrarFitxa() i revista1.MostrarFitxa() executen literalment el mateix codi
de MaterialBibliotecari. Però té sentit que la fitxa d'un llibre inclogui també el seu ISBN,
i la d'una revista, el seu número d'edició —informació que MaterialBibliotecari no coneix.
Per permetre que una classe hereva sobreescrigui el comportament d'un mètode heretat, la
classe base ha de marcar aquest mètode com a virtual, i la hereva ha de marcar la seva pròpia
versió com a override:
class MaterialBibliotecari
{
// ... propietats i constructor com abans ...
public virtual void MostrarFitxa()
{
Console.WriteLine($"Titol: {Titol}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Llibre : MaterialBibliotecari
{
public string Isbn { get; set; }
public Llibre(string titol, string autor, string isbn) : base(titol, autor)
{
Isbn = isbn;
}
public override void MostrarFitxa()
{
base.MostrarFitxa(); // executa primer la versio de MaterialBibliotecari
Console.WriteLine($"ISBN: {Isbn}"); // i afegeix la linia propia de Llibre
}
}Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
llibre1.MostrarFitxa();
// Titol: Rayuela
// Autor: Julio Cortazar
// Disponible: True
// ISBN: 978-84-376-0495-4Dins de Llibre.MostrarFitxa(), la crida base.MostrarFitxa() executa la versió original del
mètode definida a MaterialBibliotecari (les tres primeres línies), i a continuació s'afegeix
la línia específica de l'ISBN. Això evita duplicar les línies de Titol, Autor i
Disponible: es reutilitza la implementació de la classe base i només s'afegeix el que és
nou. Revista faria exactament el mateix amb el seu propi NumeroEdicio:
class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
public override void MostrarFitxa()
{
base.MostrarFitxa();
Console.WriteLine($"Numero d'edicio: {NumeroEdicio}");
}
}| Paraula clau | On s'utilitza | Què significa |
|---|---|---|
virtual |
Al mètode de la classe base | "Les classes hereves poden sobreescriure aquest mètode" |
override |
Al mètode de la classe hereva | "Aquesta és una versió pròpia d'un mètode marcat virtual a la base" |
base.Metode() |
Dins d'un mètode sobreescrit | "Executa també la versió original del mètode, definida a la classe base" |
Aquesta lliçó s'ha centrat en la mecànica d'heretar i sobreescriure; a la propera lliçó, Polimorfisme, veuràs per què aquesta capacitat de sobreescriure mètodes és molt més potent del que sembla a primera vista, quan es combina amb llistes i arrays de la classe base.
- Classes segellades amb
sealed
sealedA vegades convé impedir explícitament que una classe tingui hereves, per exemple perquè el seu
disseny no està pensat per estendre's més. Per a això s'utilitza la paraula clau sealed
abans de class:
sealed class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
public override void MostrarFitxa()
{
base.MostrarFitxa();
Console.WriteLine($"Numero d'edicio: {NumeroEdicio}");
}
}
// class RevistaDigital : Revista { }
// Error de compilacio: no es pot heretar d'una classe segellada ('sealed')Marcar Revista com a sealed deixa clar, per a qui llegeixi el codi, que no està pensada
com una nova classe base: és un punt final en aquesta branca de la jerarquia.
MaterialBibliotecari i Llibre, en canvi, es mantenen sense sealed durant la resta del
mòdul, ja que MaterialBibliotecari necessita continuar sent hereva per disseny.
- Jerarquia d'una sola herència en C#
Una limitació important —i deliberada— de C# és que una classe només pot heretar d'una classe base: no existeix l'herència múltiple de classes, a diferència d'altres aspectes del llenguatge (com les interfícies, que sí que es poden combinar diverses alhora, i que veuràs al Mòdul 4). Això significa que la jerarquia d'herència de BiblioTech forma sempre un arbre, mai una xarxa:
classDiagram
MaterialBibliotecari <|-- Llibre
MaterialBibliotecari <|-- Revista
class MaterialBibliotecari {
+string Titol
+string Autor
+bool Disponible
+Prestar()
+Retornar()
+MostrarFitxa()
}
class Llibre {
+string Isbn
+MostrarFitxa()
}
class Revista {
+int NumeroEdicio
+MostrarFitxa()
}
Totes les classes en C# —incloent MaterialBibliotecari, encara que no ho hagis escrit
explícitament— hereten, en última instància, d'una classe arrel comuna anomenada object, de
la qual provenen mètodes com ToString() que ja has fet servir indirectament en interpolar
objectes dins de cadenes de text. Això queda fora de l'abast d'aquesta lliçó, però convé saber
que la jerarquia de classes de C# sempre té, al cim, aquesta arrel comuna.
Errors Comuns i Consells
- Oblidar
base(...)quan la classe base no té constructor sense paràmetres: siMaterialBibliotecarinomés defineixMaterialBibliotecari(string titol, string autor), qualsevol classe hereva està obligada a indicar: base(...)amb aquests arguments; si no, el compilador dona un error. - Sobreescriure sense
override(o sensevirtuala la base): si el mètode de la classe base no ésvirtual, la classe hereva no pot fer serviroverride; i si el que es busca és amagar el mètode amb un altre nom sense relació real de sobreescriptura (usantnewa la signatura del mètode), el comportament resultant sol confondre i rarament és el que es pretenia. Fes servir semprevirtual/overridequan la intenció sigui sobreescriure. - Duplicar codi que ja és a la classe base: si
LlibreiRevistatornen a definirTitol,AutoroDisponiblepel seu compte, es perd l'avantatge de l'herència i es reintrodueix el problema que aquesta lliçó resol. - Marcar
sealeduna classe que sí necessites continuar estenent:sealedés una decisió de disseny deliberada; aplica-la només quan tinguis la certesa que aquesta classe no ha de tenir més hereves (comRevistaen aquest mòdul), no per costum. - Esperar herència múltiple de classes: en C# una classe només pot heretar d'una única classe base; si necessites combinar diverses capacitats independents, l'eina adequada són les interfícies, que veuràs al Mòdul 4.
Exercicis
-
Defineix la classe
MaterialBibliotecariamb les propietatsTitoliAutor(string) iDisponible(bool, amb inicialitzador de propietat atrue), un constructorMaterialBibliotecari(string titol, string autor), i un mètodevoid MostrarFitxa()que mostri les tres propietats per consola. -
Crea la classe
Llibre : MaterialBibliotecariamb la propietat addicionalIsbn(string) i un constructorLlibre(string titol, string autor, string isbn)que faci servirbase(...)per inicialitzar la part heretada. Crea un objecteLlibrei mostra el seuTitol(heretat) i el seuIsbn(propi). -
Crea la classe
Revista : MaterialBibliotecari(sensesealedper ara) amb la propietatNumeroEdicio(int) i el seu propi constructor ambbase(...). MarcaMostrarFitxa()com avirtualaMaterialBibliotecarii sobreescriu-lo aRevistaamboverride, de manera que, a més de la fitxa heretada, mostri també el número d'edició fent servirbase.MostrarFitxa().
Solucions
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 void MostrarFitxa()
{
Console.WriteLine($"Titol: {Titol}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Llibre : MaterialBibliotecari
{
public string Isbn { get; set; }
public Llibre(string titol, string autor, string isbn) : base(titol, autor)
{
Isbn = isbn;
}
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(llibre1.Titol); // Rayuela
Console.WriteLine(llibre1.Isbn); // 978-84-376-0495-4
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 void MostrarFitxa()
{
Console.WriteLine($"Titol: {Titol}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
public override void MostrarFitxa()
{
base.MostrarFitxa();
Console.WriteLine($"Numero d'edicio: {NumeroEdicio}");
}
}
Revista revista1 = new Revista("National Geographic", "Diversos autors", 302);
revista1.MostrarFitxa();
// Titol: National Geographic
// Autor: Diversos autors
// Disponible: True
// Numero d'edicio: 302
Conclusió
En aquesta lliçó has après a evitar la duplicació de codi extraient una classe base,
MaterialBibliotecari, de la qual hereten Llibre i la nova Revista mitjançant :; has fet
servir base per invocar el constructor (i el mètode) de la classe base, has sobreescrit
membres heretats amb virtual/override, has vist com sealed impedeix que una classe tingui
hereves, i per què C# només permet heretar d'una única classe base alhora. El model de domini
de BiblioTech ara té una jerarquia real: MaterialBibliotecari a la base, amb Llibre i
Revista com a especialitzacions concretes.
Tanmateix, encara no has explotat la veritable potència d'aquesta jerarquia: poder tractar un
Llibre i una Revista de forma uniforme, com si tots dos fossin simplement "un
MaterialBibliotecari", i deixar que cadascun es comporti segons el seu propi tipus en temps
d'execució. Això és exactament el polimorfisme, el tema de la propera lliçó.
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
