PROGRAMMAZIONE A OGGETTI (OOP) Lezione 8 prj...
Transcript of PROGRAMMAZIONE A OGGETTI (OOP) Lezione 8 prj...
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.
Invece è possibile per una classe base avere più classi derivate nello stesso livello.
CLASSE BASE
CLASSE DERIVATA
CLASSE BASE1 CLASSE BASE2
CLASSE DERIVATA
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
publicLe classi derivate hanno accesso a questi membri, come tutto il codice esterno alla classe.
protectedLe classi derivate hanno accesso ai membri dichiarati “protected” ma il codice esterno no.
privateLe 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”
//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"); }
}//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”.
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.
La classe “FiguraGeometrica” definisce una organizzazione, una base comune e condivisa fra tutte le figure piane che vogliamo rappresentare.
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;
//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!"); Console.ReadLine(); Environment.Exit(-1); } Console.WriteLine("\n Stai creando un TRIANGOLO generico"); this.a = pa; this.b = pb; this.c = pc; Perimetro = a + b + c; this.NumeroLati = 3; }
//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; }
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!
Poiché ora so che sto creando un triangolo, è corretto definire i dati membro per rappresentarne i 3 lati.
Questa funzione “sovrascrive” quella definita nella classe base, occorre perciò avvisare il compilatore che ciò è voluto, questo si fa con la parola chiave “override”.
Questo metodo invece è destinato ad essere sovrascritto nella classe “TriangoloRetto” che sarà derivato dalla classe “Triangolo”, per cui viene dichiarato “virtual” nella classe base.
La parola chiave “this” identifica l'oggetto stesso. In questo caso avremmo anche potuto scrivere a= pa; b = pb; c = pc;
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; }
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(); Environment.Exit(-1); } }
}// 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
Questa funzione “sovrascrive” quella definita nella classe base, occorre perciò avvisare il compilatore che ciò è voluto, questo si fa con la parola chiave “override”.
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.
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()); //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
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());
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.
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.
Come premesso usiamo gli stessi lati già inseriti per motivi di praticità.
In questo caso invece chiediamo i lati di un rettangolo e lo creiamo.
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