A. Veneziani - Alcune note sull'ereditarietà in C#

Pagina creata da Valeria Iorio
 
CONTINUA A LEGGERE
A. Veneziani – Alcune note sull’ereditarietà in C#

Cos’è l’ereditarietà
Per ereditarietà si intende un meccanismo sintattico (presente in molti linguaggi quali Object Pascal C++, Java
ed altri), tramite il quale le classi vengono messe poste in gerarchia fra loro.
Il meccanismo gerarchico, oltre a definire un collegamento logico tra esse, definisce una maggior o minor
specializzazione delle classi appartenenti ad una stessa famiglia (in quanto orientate a rappresentare oggetti
simili) o un minor o oppure più ampio ventaglio di funzionalità (estensione) offerte da classi che hanno però
caratteristiche comuni. In pratica si può dire che l’ereditarietà viene utilizzata quindi nei casi si desideri
effettuare una estensione di una classe, oppure alternativamente creare una versione più specializzata di una
classe già esistente.

Ereditarietà semplice
Con ereditarietà semplice si indica una situazione in cui una sottoclasse può ereditare le sue funzionalità da
una sola classe, ossia quella che chiamiamo la sua superclasse. In pratica in questo schema di ereditarietà,
ogni superclasse può avere una o più sottoclassi da essa dipendenti, ma ogni sottoclasse ha una ed un'unica
superclasse. C# segue questo tipo di regola per organizzare l’ereditarietà delle classi, analogamente a
numerosi altri linguaggi tra cui Object Pascal e Java.
Alcuni linguaggi permettono invece una architettura più generale di ereditarietà, per la quale una sottoclasse
può eventualmente dipendere da più superclassi (oltre alla situazione, usuale, per cui da una superclasse
possono derivare più sottoclassi). Tipicamente C++ è uno dei linguaggi ad oggetti che adotta questo tipo di
organizzazione. In questo caso si parla di ereditarietà multipla.

Un esempio
Consideriamo, come già in passato la classe Contatore.            Si immagini di considerare tale classe che
rappresenterà oggetti capaci di tenere memoria di un conteggio in base a quante volte un apposito metodo
( incrementa() ), sia richiamato. Prevediamo anche la possibilità di un azzeramento del contatore tramite il
metodo azzera().
Si immagini poi di creare una estensione di Contatore (chiamata ContatoreExt) che rappresenti un contatore
con le stesse caratteristiche, ed aggiunte ad esse la capacità di settare un valore iniziale, effettuare
decrementi ed un controllo sul raggiungimento del valore 0 nel contatore.
In sintesi la classe Contatore possiede i metodi:
      incrementa
      azzera
      leggi_conteggio
E supponiamo che la classe ContatoreExt sua estensione implementi anche i metodi:
      setta valore iniziale
      decrementa
      e_zero
In questa situazione, ossia se ContatoreExt è programmata come sottoclasse di Contatore, si avrà che la
classe ContatoreExt sarà dotata anche dei metodi di Contatore, in quanto sua estensione.
Si dice per converso che ContatoreExt è una sottoclasse (o classe derivata) di Contatore.
Si noti anche che in questo caso i metodi di ContatoreExt si aggiungono semplicemente a quelli di Contatore,
senza per questo dover riutilizzarne il codice.
In questo specifico caso ContatoreExt si può considerare effettivamente solo una semplice estensione di
Contatore, dato che ContatoreExt aggiunge semplicemente funzionalità.
In altri casi una sottoclasse è effettivamente una specializzazione della relativa superclasse.

                                                  Pagina 1
Sottoclasse come specializzazione
Per evidenziare le situazioni in cui una sottoclasse non è altro che la gestione di un caso particolare di una
superclasse, possiamo considerare un altro esempio spesso utilizzato.
Consideriamo la classe Rettangolo come classe implementante le caratteristiche dei rettangoli ed alcune
operazioni possibili su di essi. Consideriamo ora una classe che raggruppi una categoria molto particolare di
rettangoli, ossia i quadrati. Possiamo pensare ad una classe Quadrato come ad una sottoclasse di Rettangolo,
e questo in quanto caso specifico della classe Rettangolo stessa.
Ad esempio si potrebbe pensare Rettangolo come avente i metodi:
     area
     perimetro
     diagonale (lunghezza della diagonale)
     rapporto_bh (rapporto tra base ed altezza)
Ma se pensiamo ad una classe Quadrato, anche per essa potremo pensare a metodi del tutto simili:
     area
     perimetro
     diagonale
solo che essi dovranno essere calcolati sul caso specifico di un certo quadrato.
In realtà Quadrato è evidentemente solo un caso specifico di Rettangolo ed essa può ri-utilizzare in modo
ampio il codice della sua superclasse, almeno per re-implementare metodi del tutto simili nello scopo.
Infatti un quadrato non è altro che un rettangolo con entrambi i lati uguali. Quindi:
     Il costruttore di Quadrato potrà appoggiarsi su quello di Rettangolo
     Quadrato potrà utilizzare le variabili globali contenute in Rettangolo
     I metodi area() e perimetro() potranno essere calcolati utilizzando il codice già presente nella classe
         Rettangolo
Diamo ora alcune regole sintattiche che permettano di realizzare effettivamente in C#, i modelli prima
proposti di classi in gerarchia.

Sottoclasse
In C# per definire una classe C2 come sottoclasse di un'altra (C1) si usa la semplice sintassi:
        class C1 { . . . }
        class C2 : C1
        {
             ...
        }
Dato questo la classe C2 eredita tutti i metodi (di tipo public o eventualmente protected) di C1, ossia ad un
oggetto di C2 saranno applicabili tutti i metodi della classe C1 di tipo public o protected.
Inoltre essa potrà implementare nuovi metodi specifici propri di C2, o ridefinire metodi (modificandoli) già
presenti in C1, tramite overloading ed overriding. I metodi definiti in C2, viceversa non saranno visibili nella
superclasse C1.
C’è da evidenziare la regola generale che i costruttori non vengono ereditati.

La specifica protected
Per quanto riguarda le variabili di stato della classe C1, esse saranno visibili ed utilizzabili in C2, ma solo se
esse siano di tipo protected (oppure, ovviamente, se fossero public).
Variabili con specifica private non saranno visibili nelle classi derivate. In modo analogo non saranno
utilizzabili in classi derivate metodi di tipo private.
                                                    Pagina 2
Il tipo di visibilità protected è una particolare specifica di visibilità che rende possibile utilizzare metodi o
variabili globali ad una classe, anche nelle sottoclassi della classe in cui lo specificatore di visibilità protected
compare, permettendone l’utilizzo in gerarchie di classi, senza allargare il campo di visibilità a livello generale
equivalente alla indicazione public.

Costruttori
Ripetuta la regola principale che i costruttori non sono soggetti alla ereditarietà, ossia non vengono ereditati
dalle sottoclassi, consideriamo alcune regole relative ai costruttori di classi in relazione gerarchica tra loro:
    a) Qualunque costruttore di una sottoclasse richiama sempre un costruttore relativo alla superclasse.
    b) Una sottoclasse senza altra specifica riguardo ai costruttori possiede un costruttore sottinteso di
        default che opera senza parametri e compie l’unica operazione di allocare oggetti della sottoclasse.
        Tale costruttore richiama in modo implicito, in cascata, anche il costruttore di default o un
        costruttore senza parametri della relativa superclasse.
    c) Nel caso si desideri definire costruttori diversi da quello di default nella sottoclasse ciò è ovviamente
        possibile (spesso questi costruttori operano allo scopo di inizializzare le variabili interne dell’oggetto
        creato). Un costruttore definito ex-novo in una sottoclasse possiede genericamente una chiamata
        in modo implicito al costruttore di default della superclasse.
    d) Nell’ulteriore caso che nella superclasse sia stato definito un solo costruttore con parametri, allora
        per creare un oggetto della superclasse è necessaria una chiamata esplicita al costruttore stesso della
        superclasse.
        In questo ultimo caso, se richiamato, un costruttore della sottoclasse deve indicare esplicitamente la
        chiamata al costruttore con parametri della superclasse, in questo modo:
                  class C2 : C1
                  {
                       public C2(int n) : base(n)
                       { . . . }
                  }
    e) Ricordiamo inoltre un'altra regola che vale per tutte le classi; la definizione di un costruttore in una
        classe, elimina in modo automatico la definizione (sottintesa) del costruttore di default della classe.
        Quindi, in particolare, in una classe se viene definito un, primo, nuovo costruttore con uno o più
        parametri, in tale classe non esisterà più il costruttore (di default) senza parametri fino a che esso
        non sia eventualmente ridefinito opportunamente.
Da ciò ne consegue che un costruttore in una classe derivata del tipo:
        class C1
        {
             (costruttore sottinteso di default)
              . . .
        }
        class C2 : C1
        {
              public C2(int n)
              { . . . }
        }
Se in C1 si definirà un costruttore con parametri:
        class C1
        {
             int n;
            public C1(int v)

                                                     Pagina 3
{ n = v; }
        }
        class C2 : C1
        {
             public C2(int n)
             { . . . }
        }
La chiamata al costruttore di C2 darà l’errore:
“ Error does not contain a constructor that takes 0 arguments”
Ossia segnalando che il costruttore senza parametri di default non è più definito.

Accesso a variabili della superclasse
L’accesso a variabili di tipo protected di una superclasse è sempre possibile indicando semplicemente il nome
delle stesse nel codice della sottoclasse. Come già detto le variabili private (o senza specifica, quindi
considerate di default private in C#) non saranno accessibili.

Metodi ereditati
Come si è già detto i metodi public o protected di una superclasse vengono ereditati dalle sue sottoclassi e
quindi sono visibili ed utilizzabili dalle sottoclassi stesse. Metodi private non sono ereditati dalle sottoclassi.

Omonimia e keyword base
In caso di omonimia tra una variabile globale ed il parametro di un metodo, si avrà che il parametro “coprirà”
(hide) la omonima variabile globale della classe corrente o della superclasse.
Tuttavia anche in questo caso la variabile resta accessibile utilizzando:
      this. per le variabili (omonime) della classe corrente
      base. per le variabili (omonime) della superclasse.
La parola chiave base viene anche utilizzata:
      per chiamare metodi sottoposti ad override presenti nella classe base, tramite la sintassi:
                 base.
      specificare quale costruttore della classe base debba essere richiamato, relativamente ad un certo
         costruttore della sottoclasse

Upcasting
Si parla di upcasting, quando ottenuto un oggetto di una sottoclasse, ed un riferimento ad esso, si tenti di
assegnare il riferimento a tale oggetto ad una variabile riferimento del tipo della superclasse.
Questa operazione è di norma possibile operando una semplice assegnazione e senza l’uso di cast.
Ad esempio:
        class Animale
        { . . . }

        class Leone : Animale
        { . . . }

        class Program
        {
           static void Main(string[] args)
           {
             Animale a = new Animale();
             Leone l = new Leone();

              a = l;    // la variabile a è caricabile con l’oggetto di tipo Leone
                        // (che rimane tale)
                                                    Pagina 4
E’ da evidenziare che la natura intrinseca dell’oggetto non cambia (ossia il linguaggio ha memoria che
l’oggetto è originariamente di tipo Leone), ma semplicemente per esso cambia formalmente solo il tipo della
variabile riferimento.

Downcasting
Con questo termine si intende il problema opposto a quello precedente. Presa una variabile riferimento ad
un oggetto di una superclasse, è possibile convertire lo stesso oggetto al tipo di una sottoclasse ?
Usualmente il downcasting non è possibile in modo diretto (ossia utilizzando le usuali indicazioni di cast). In
tal caso si ottiene un errore a run-time di InvalidCastException. Come scritto nella documentazione del
linguaggio C#, questo tipo di errore abbisogna di solito di una modifica alle operazioni svolte dal codice (ossia
specificamente rimuovere o condizionare il tentativo di conversione in questione) piuttosto che una usuale
gestione della eccezione tramite blocco try / catch.
Un caso in cui il downcasting è possibile è quello in cui l’oggetto riferito dalla variabile della superclasse sia
in realtà un oggetto della sottoclasse.
        static void Main(string[] args)
         {
             Animale a = new Animale();
             Leone l = new Leone(), l2;

              // l = (Leone) a;         //   questo cast non è eseguibile a è Animale e non è
                                        //   convertibile
                                        //   ossia un puntatore alla sottoclasse non può
                                        //   puntare all'oggetto
                                        //   della superclasse

              a = l;     // a può fare da riferimento per un oggetto più specializzato (leone)
                         // quindi ora a "punta" a un leone, ma essendo i metodi ridefiniti...
                         // (upcast)

              if (a is Leone)            // qui ho il controllo se il tipo di a sia convertibile
                  l2 = (Leone) a;        // qui il downcast è possibile in quanto a punta già
                                         // ad una variabile in realtà di tipo Leone
              ...
         }

Si deduce anche dall’esempio che tramite la parola chiave is è possibile controllare il tipo effettivo di un
oggetto (al di là del tipo della varaibile riferimento).

Overloading di metodi
Si dice che un metodo viene sottoposto ad overloading se viene scritta una sua versione mantenendo lo
stesso nome, ma utilizzando tipo o numero di parametri diverso (ossia si scrive un metodo simile, ed appunto
perché il suo scopo è analogo non ha senso cambiarne il nome). L’overloading di un metodo può avvenire
in una stessa classe o in una superclasse ed in sue sottoclassi.

Sovrapposizione di un metodo (Method hiding)
Un metodo può essere ridefinito in sottoclassi in relazione di ereditarietà con una superclasse. In tal caso
un metodo con stesso nome e stesso tipo e numero di parametri, definito nella sottoclasse, “nasconderà” e
sostituirà il suo omologo presente nella superclasse.
In presenza di più sottoclassi, nelle sottoclassi dove il metodo non sia stato sostituito, resterà visibile il
metodo originario della superclasse.
In questa situazione la sintassi C# prevede che si inserisca una denominazione new anteposta al nome del
metodo; ad esempio:

                                                    Pagina 5
class A
    {
        protected int n;

         public void cambia()
         {
             n++;
         }
    }

    class B : A
    {
        public new void cambia()
        {
            n = n + 2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            A a;
            A a2 = new A();

              a = b;
              a.cambia();
              b.cambia();
                ...

       Nella chiamata ad a.cambia() il metodo eseguito sarà quello della superclasse A.
       Viceversa nella chiamata a b.cambia() eseguirà il metodo nella sottoclasse B.
       Nella chiamata ad a2.cambia() il metodo eseguito sarà quello della superclasse A.

Con questa sintassi ad ogni oggetto viene applicato il metodo relativo solo alla classe a cui appartiene il
riferimento ad esso.
La presenza di un hiding di metodi comporta quindi che se un riferimento ad oggetto della classe base, punta
ad un oggetto di una sottoclasse, esso eseguirà il metodo presente nella classe base.
Infine c’è da osservare che nell’hiding di metodi non è tassativo che il tipo ritornato dal metodo coincida.

Overriding di un metodo
In altri casi si desidera ridefinire un metodo di una superclasse in una sottoclasse. La ridefinizione di un
metodo prevede l’utilizzo dello stesso nome e di numero e tipo coincidenti di parametri.
La sintassi C# che permette di definire che un metodo e ridefinito è:
    a) Inserire la parola chiave virtual prima del nome del metodo della superclasse ridefinito
    b) Inserire la parola chiave override prima del nome del metodo che nella sottoclasse ridefinisce il
         metodo della superclasse.
La presenza di un override di metodi comporta che se un riferimento ad oggetto della classe base, punta ad
un oggetto di una sottoclasse, esso eseguirà il metodo presente nella classe derivata (sottoclasse).
Inoltre vi è da sottolineare che in C# non è possibile effettuare overriding di metodi se il tipo ritornato non
coicide.
Vediamo un analogo codice di esempio contenente un caso di overriding:
using System;

                                                  Pagina 6
namespace ConsoleApplication1
{
    class A
    {
        protected int n;

         public virtual void cambia2()
         {
             n++;
         }
    }

    class B : A
    {
        public override void cambia2()
        {
            n = n + 2;
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            A a;
            A a2 = new A();

             a = b;
             a.cambia2();
             b.cambia2();
             a2.cambia2();

Ciò che viene eseguito date le relative chiamate al metodo cambia2() è:
      a.cambia2(); esegue il metodo cambia2() della classe B
      b.cambia2(); esegue il metodo cambia2() della classe B
      a2.cambia2(); esegue il metodo cambia2() della classe A
in pratica il metodo effettivamente eseguito, in caso di override, dipende solo dalla effettiva natura
dell’oggetto.
In realtà il riferimento a in realtà si riferisce ad un oggetto della classe B. Nella seconda situazione il
riferimento b “punta” ad un oggetto di tipo B.
In entrambi casi il metodo cambia2() eseguito è quindi quello della classe B.
Nell’ultimo caso il riferimento a2 è di tipo A, ma così anche l’oggetto puntato da esso, quindi il metodo
cambia2() che viene eseguito è quello della classe A.

Polimorfismo
Il comportamento rilevato nel paragrafo precedente, ossia la selezione del metodo da eseguire
esclusivamente in base alla reale natura (classe) dell’oggetto a cui si applica tale metodo è detto
polimorfismo.

Overloading di operatori
Un argomento collaterale, che ha però ampie aree di uso ed ha a che fare con la OOP è l’overloding di
operatori (in inglese operator overloading).

                                                 Pagina 7
In pratica si tratta di una tecnica di programmazione che permette di utilizzare i vari operatori già presenti
nel linguaggio per vari scopi, e rimpostarli di modo che possano essere utilizzati su nuovi tipi di dato definiti
dal programmatore.
Gli operatori si suddividono in due grandi categorie:
      unari, ossia che ha senso applicare ad un solo operando (ad esempio l’operatore ! (not – negato)
      binari ossia che si applicano a due operandi (ad esempio l’operatore + (somma)
Una delle regole dell’operator overloading è che operatori unari dovranno essere ridefiniti in modo che
operino nuovamente come unari e operatori binari dovranno essere ridefiniti in modo che operino
nuovamente come binari.
Inoltre non è possibile definire operatori diversi da quelli già definiti precedentemente per qualche scopo.
La sintassi per ri-definire un nuovo operatore è:

          public static  operator  ()
per esempio il metodo sottostante ridefinisce il ben noto operatore - per abbinarlo alla operazione di
differenza tra insiemi:
         public static Insieme operator-(InsiemeExt i1, InsiemeExt i2)
          {
                ....

Si parla per questa ridefinizione di overloading perché gli operatori vengono paragonati a funzioni il cui nome
è il medesimo, i cui parametri sono sì in numero uguale (1 o 2), ma il tipo di essi è ogni volta diverso per ogni
definizione effettuata.

                                                   Pagina 8
Puoi anche leggere