PROGRAMMAZIONE A OGGETTI (OOP) Lezione 8 prj Mesa (Prof. Ing N. Muto)
Approfondimento EREDITARIETA'
In questa lezione trattiamo il terzo ed ultimo fattore caratteristico della programmazione a
oggetti, ossia la possibilità di implementare classi a partire da altre classi implementate in
precedenza.
Oltre al vantaggio di poter riutilizzare codice già scritto, l'ereditarietà consente una
organizzazione efficientissima del codice, potendo creare delle gerarchie di classi, estendibile
finché si vuole oltre semplici e veloci da manutenere. Come già anticipato nella lezione XXX la
classe creata prima viene denominata classe “base” o “madre” mentre la classe derivata
prende il nome di “derivata” o “figlia” e quest'ultima può diventare a sua volta classe base per
una successiva classe derivata e così via. Una delle differenze del linguaggio C# con altri
linguaggi OOP riguardo all'ereditarietà è che in C# una classe figlia può avere SOLO una
classe base e non più di una.
Rappresentando il concetto in forma grafica quindi si ha che la soluzione a sinistra è
ammessa mentre quella a destra no.
CLASSE BASE
CLASSE
DERIVATA
CLASSE BASE1
CLASSE BASE2
CLASSE
DERIVATA
Invece è possibile per una classe base avere più classi derivate nello stesso livello.
La classe derivata “eredita” tutti i membri della classe base ma possono essere imposte
delle condizioni per motivi di sicurezza e praticità usando gli stessi modificatori di accesso o
visibilità già incontrati finora con l'aggiunto di uno nuovo: “protected”; in particolare sono valide
le regole riassunte dalla seguente tabella:
Modificatore di visibilità
Descrizione
public
Le classi derivate hanno
accesso a questi membri,
come tutto il codice esterno
alla classe.
protected
Le classi derivate hanno
accesso ai membri dichiarati
“protected” ma il codice
esterno no.
private
Le classi derivate NON
hanno accesso a questi
membri, così come il codice
esterno.
Una classe base potrebbe essere creata solo per motivi organizzativi, senza contenere
nessuna implementazione di metodi, in questo caso occorre usare la parola chiave “abstract”,
sarà poi la classe derivata a implementare i metodi in modo corretto. Ad esempio se dobbiamo
creare una raccolta di codice per gestire delle figure geometriche piane, potremmo decidere
per motivi organizzativi di creare una classe base “FiguraGeometrica” che conterrà dei metodi
“CalcolaArea” e “CalcolaPerimetro” anche se di fatto non sapremo come fare a calcolarli
perché abbiamo solo creato il concetto di “figura geometrica”, e quindi siamo rimasti ad un
livello “astratto” appunto!
Quando avremo derivato la classe “Quadrilatero” oppure la classe “Triangolo” dalla classe
base, allora potremo definire il metodo “CalcolaPerimetro” mentre non è detto che sappiamo
come calcolare l'area, per la quale dovremo aspettare di avere un ulteriore livello di
derivazione, come ad esempio “Rettangolo”, oppure TriangoloRettangolo”. In questo esempio
possiamo anche capire quindi come l'ereditarietà consenta una organizzazione che dal
generale scende mano a mano nello specifico, permettendo di definire in modo più dettagliato
le proprietà degli oggetti derivati.
Un altra situazione che potremmo avere è quella di una classe base che contiene dei
metodi implementati, da cui si deriva una classe derivata che ha bisogno di personalizzare
qualcuno dei metodi ereditati dalla classe base. Questo si può fare usando le parole chiave
“virtual” ed “override”, come vedremo nell'esempio di codice che seguirà a breve. Qui
possiamo intanto anticipare che “virtual” si userà nella classe base in corrispondenza del
metodo che potrà essere “riscritto” dalla classe derivata, invece “override” si userà nella classe
derivata quando andremo a riscrivere il metodo suddetto.
Per inciso possiamo notare che questa situazione altro non è se non un caso di
“polimorfismo”, applicato all'ereditarietà. Infatti classe base e classe derivata usano sempre lo
stesso nome per indicare codice differente, sarà poi il compilatore ad eseguire il giusto
collegamento poiché conosce da dove è derivato l'oggetto chiamato.
Ora è arrivato il momento di mettere in pratica quanto finora definito per cui decidiamo di
realizzare un codice per gestire le figure geometriche piane organizzando il suo sviluppo
usando le classi con l'ereditarietà. Decidiamo di partire da una classe base che rappresenti il
concetto astratto di “Figura Geometrica” ma che contengo comunque dei dati membro, da
questa deriveremo due classi figlie ossia “Triangolo” e” “Rettangolo”. Successivamente dalla
classe “Triangolo” deriveremo una ulteriore classe specializzata, ossia “TriangoloRettangolo”
La classe “FiguraGeometrica”
definisce una organizzazione,
una base comune e condivisa
fra tutte le figure piane che
vogliamo rappresentare.
//classe base astratta per definire una figura geometrica piana
public abstract class FiguraGeometrica2D {
//dati membro utilizzabili dalle classi derivate ma non dal codice esterno alla classe
protected float Area;
public float Perimetro;
public int NumeroLati;
//questa è un metodo astratto e si può evitare di definirne il corpo
public abstract float CalcolaArea();
public FiguraGeometrica2D() {
Console.WriteLine("\n Stai creando una figura geometrica 2D");
}
Definendo una funzione “abstract” si
può evitare di definirne il corpo. In
effetti non sapremmo cosa scrivere in
questo metodo perché la classe non
rappresenta al momento NESSUNA
figura specifica.
}//fine della classe FiguraGeometrica
La scritta “Console.WriteLine("\n Stai creando una figura geometrica 2D");” nel costruttore è stata
inserita per facilitare allo studente nel seguire le varie fasi di creazione degli oggetti.
Nella prossima pagina vedremo l'esempio di una prima classe derivata, ossia la classe
“Triangolo”.
public class Triangolo : FiguraGeometrica2D {
//definisco i dati membro per i lati
//invece Area, Perimetro e NumeroLati vengono ereditate
public float a;
public float b;
public float c;
Poiché ora so che sto creando
un triangolo, è corretto definire
i dati membro per
rappresentarne i 3 lati.
//Scrivo il costruttore
public Triangolo(float pa, float pb, float pc)
{
//controlliamo che ogni lato sia minore della somma degli altri due, altrimenti il programma termina
if (!((pa < pb + pc) && (pb < pa + pc) && (pc < pb + pa)))
{
Console.WriteLine("\n I dati inseriti non consentono la creazione di un triangolo!");
La parola chiave “this” identifica l'oggetto
Console.ReadLine();
stesso. In questo caso avremmo anche
Environment.Exit(-1);
potuto scrivere
}
a= pa;
Console.WriteLine("\n Stai creando un TRIANGOLO generico"); b = pb;
this.a = pa;
c = pc;
this.b = pb;
Questa funzione “sovrascrive” quella
this.c = pc;
definita nella classe base, occorre
Perimetro = a + b + c;
perciò avvisare il compilatore che ciò è
this.NumeroLati = 3;
voluto, questo si fa con la parola chiave
“override”.
}
//la funzione per calcolare l'area posso scriverla usando la formula di Erone
public override float CalcolaArea() {
float p = Perimetro / 2;
Area = (float)Math.Sqrt(p*(p-a)*(p-b)*(p-c));
return Area;
}
Questo metodo invece è destinato ad essere
sovrascritto nella classe “TriangoloRetto” che
sarà derivato dalla classe “Triangolo”, per cui
viene dichiarato “virtual” nella classe base.
public virtual void CheckRetto() {
Console.WriteLine("Metodo che deve essere sovrascritto dalla classe derivata");
}
}//fine della classe Triangolo
In questo esempio vediamo quindi anche come si “sovrascrive” una funzione avente lo
stesso nome di un'altra (altro esempio di polimorfismo) e come ci si predispone a crearne una
“segnaposto” da far scrivere realmente alla classe figlia.
Nella prossima pagina vedremo l'ultima classe derivata: TriangoloRettangolo”, arrivando
così a ben 3 livelli gerarchici!
public class TriangoloRettangolo : Triangolo {
public TriangoloRettangolo(float pra, float prb, float prc)
: base(pra, prb, prc)
//questa istruzione richiama il costruttore della classe base di TriangoloRettangolo
{
Console.WriteLine("\n Stai creando un TRIANGOLO RETTANGOLO, i primi due lati si considerano cateti");
}
//In questa classe posso riscrivere il metodo per calcolare l'Area
public override float CalcolaArea()
{
float A = a * b/2;
return A;
}
Notiamo che con questa notazione “: base...”
abbiamo richiamato il costruttore della classe
base di TriangoloRettangolo, che altrimenti
non avrebbe potuto inizializzare
correttamente l'oggetto stesso.
La funzione “CalcolaArea()” viene
come al solito sovrascritta per usare
una formula specifica dell'oggetto in
questione.
public override void CheckRetto()
{
Console.WriteLine("\n Metodo della classe derivata");
if(!((a*a == b*b+c*c)||(b*b == a*a+c*c)||(c*c == a*a+b*b))){
Console.WriteLine("\n I dati inseriti non sono di un triangolo rettangolo!");
Console.ReadLine();
Questa funzione “sovrascrive” quella
Environment.Exit(-1);
definita nella classe base, occorre
}
}
perciò avvisare il compilatore che ciò è
voluto, questo si fa con la parola chiave
“override”.
}// fine della classe TriangoloRettangolo
Vediamo ora, per completezza, una classe Rettangolo creat a partire dalla classe base astratta
"FiguraGeometrica"; non mettiamo alcun commento per stimolare il lettore a comprenderla da
solo usando le informazioni già abbondantemente fornite negli esempi precedenti.
public class Rettangolo : FiguraGeometrica2D
{
//definisco i dati membro per i lati
//invece Area, Perimetro e NumeroLati vengono ereditate
public float a;
public float b;
//Scrivo il costruttore
public Rettangolo(float pa, float pb)
{
Console.WriteLine("\n Stai creando un RETTANGOLO ");
this.a = pa;
this.b = pb;
Perimetro = 2*(a + b);
this.NumeroLati = 4;
}
public override float CalcolaArea()
{
Area = a * b;
return Area;
}
}//fine della classe Rettangolo
Come esercizio si propone di derivare una classe Quadrilatero dalla classe FiguraGeometrica2D e
poi dalla classe Quadrilatero derivare una classe Quadrato.
Aggiungiamo a questa lezione il modificatore “sealed”, applicabile alla dichiarazione di una
classe, quando si vuole che da tale classe NON sia possibile derivarne altre:
sealed class TriangoloEquilatero : Triangolo {
}
Se provassimo infatti a scrivere class TriangoloPippo : TriangoloEquilatero, il compilatore ci
darebbe un messaggio in cui ci avvisa che TriangoloPippo non può derivare da
TriangoloEquilatero. Anche questa è una caratteristica che risulta utile per l'organizzazione e
la sicurezza del codice, specie quando il codice deve girare fra vari programmatori.
Analizziamo infine il codice scritto per creare concretamente gli oggetti e vederli in azione:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ereditarieta
{
class Program
{
static void Main(string[] args)
{
//Faccio scegliere all'utente le misure dei lati del triangolo
Console.WriteLine("\n Inserisci i tre lati del triangolo: ");
Console.Write("\n a: ");
float La = Convert.ToSingle(Console.ReadLine());
Console.Write("\n b: ");
float Lb = Convert.ToSingle(Console.ReadLine());
Console.Write("\n c: ");
float Lc = Convert.ToSingle(Console.ReadLine());
//Creo un'oggetto Triangolo
Triangolo myTg = new Triangolo(La, Lb, Lc);
Console.WriteLine("\n Il perimetro vale: {0} e l'area vale: {1}", myTg.Perimetro, myTg.CalcolaArea());
Console.ReadLine();
}
In questo primo "runtime" ci limiteremo a creare un triangolo generico per cui, una volta
lanciato, il programma chiederà i dati e produrrà la seguente uscita:
Le scritte
"Stai creando... 2D"
e "Stai creando..."
ci mostrano i costruttori
in azione.
Modificando il main con altri oggetti, possiamo vedere la gerarchia completa di classi in
azione:
static void Main(string[] args)
{
//Faccio scegliere all'utente le misure dei lati del triangolo
Console.WriteLine("\n Inserisci i tre lati del triangolo: ");
Console.Write("\n a: ");
float La = Convert.ToSingle(Console.ReadLine());
Console.Write("\n b: ");
float Lb = Convert.ToSingle(Console.ReadLine());
Console.Write("\n c: ");
float Lc = Convert.ToSingle(Console.ReadLine());
Per il primo oggetto triangolo chiedo i
lati. Inseriremo i lati di un triangolo
rettangolo(3,4,5), in modo che
possiamo passare tali valori
direttamente al secondo oggetto, senza
doverli chiedere ancora.
//Creo un'oggetto Triangolo
Triangolo myTg = new Triangolo(La, Lb, Lc);
Console.WriteLine("\n Il perimetro vale: {0} e l'area vale: {1}", myTg.Perimetro, myTg.CalcolaArea());
//Ora creo un triangolo Rettangolo
Come premesso usiamo gli stessi lati
già inseriti per motivi di praticità.
TriangoloRettangolo myTr = new TriangoloRettangolo(La,Lb,Lc);
myTr.CheckRetto();
Console.WriteLine("\n Il perimetro vale: {0} e l'area vale: {1}\n\n", myTr.Perimetro, myTr.CalcolaArea());
//Ora creo un Rettangolo
Console.WriteLine("\n Inserisci base e altezza del rettangolo: ");
Console.Write("\n a: ");
La = Convert.ToSingle(Console.ReadLine());
Console.Write("\n b: ");
Lb = Convert.ToSingle(Console.ReadLine());
In questo caso invece chiediamo i lati
di un rettangolo e lo creiamo.
Rettangolo myR = new Rettangolo(La, Lb);
Console.WriteLine("\n Il perimetro vale: {0} e l'area vale: {1}", myR.Perimetro, myR.CalcolaArea());
Console.ReadLine()
}
La pagina seguente mostra l'output completo del programma, notare le scritte didattiche
che indicano quando i costruttori agiscono e a chi appartengono i metodi chiamati.
Le tre scritte sono
rispettivamente: del costruttore
della classe base astratta, del
costruttore della prima classe
derivata Triangolo e del
costruttore della seconda classe
derivata “TriangoloRettangolo”
Scritta del costruttore della classe
base e del costruttore della
derivata