In questi giorni di vacanza mi sono ritrovato a leggere alcuni articoli che riguardavano i vari metodi per implementare l'hiding e l'override dei metodi di una classe in C#.

La questione personalmente mi è sempre sembrata pedante e assolutamente poco interessate, ma da contraltare spesso mi sono accorto che questa parte nasconde diverse insidie: per tale motivo ho pensato di scriverne un post sopratutto per averne un memento.

Inizierà il post espondendo alcune note iniziali al fine di inquadrare meglio il problema.

La questione in analisi riguarda la situazione in cui si scrive un metodo in una classe base, e in una classe da questa derivata, si riscrive un metodo con stessa firma (cioè con stesso, lista parametri e valore di ritorno).

Oss.: Qui si parlerà di metodi, ma tutto quanto esposto vale anche per le proprietà esposte dalla classe.

Occorre dire che in situazione "normali" il metodo implementato all'interno della classe derivata “sovrascrive” il metodo della classe base: detto in altri termini se instanzia la classe derivata il metodo sovrasrcitto della classe base verrà ignorato, e verrà eseguito, come ci si aspetta (?) , il codice implementato all'interno del metodo della classe derivata.

 

public class BaseClass
{
  public void MetodoRiscritto()
  {
    Console.WriteLine("Esecuzione da classe Base");
} } public class DerivedClass : BaseClass { public void MetodoRiscritto() { Console.WriteLine("Esecuzione da classe Derivata");
} }

In un caso come quello sopra se si instanza la classe Derived e si richiama il metodo MetodoSovrascittto si otterrà il seguente risultato.

DerivedClass d = new DerivedClass();
d.MetodoRiscritto();
...
> Esecuzione da classe Derivata

 Sin qui tutto nella nella logica: ma cosa succede se si esegue una serie di istruzioni come quella nel seguito ?

BaseClass b = new DerivedClass();
b.MetodoRiscritto();

In un caso come quello sopra quale delle due implementazioni di MetodoRiscritto sarebbe eseguito ?? La risposta è che sarebbe eseguito quello della classe base.

Occorre anche osservare che tutti i metodi eventualmente presenti solo nella classe derivata non sarebbero visibili all'instanza della classe base b: questo per ulteriormente rafforzare il fatto che i metodi della classe derivata sono semplicemente ignorati. In questo caso, infatti, l'unica cosa che viene utilizzata della classe derivata è il suo costruttore.

DerivedClass d = new DerivedClass();
BaseClass b1 = ((BaseClass)d);
b1.MetodoRiscritto();

Nel caso sopra, invece, la variabile d "vedrebbe" tutti i metodi presenti nella sola classe derivata ma, curiosamente l'istanza b1 sarebbe in grado di richiamere il metodo implementato nella classe base.

Vedendo quanto sopra è possibile comprendere come, in situazioni complesse con oggetti molto articolati e dotati di svariati metodi e proprietà, magari a fronte di diversi sviluppatori che lavorano al codice di un progetto, rendono la possibilità di ottenere comportamenti anomali del codice, a seguito di sovrascritture improvvide di metodi o proprietà, un’eventualità tutt'altro che remota.

Metod Override

Dichiarando un metodo della classe base come virtual si permette esplicitamente alle classi derivate di “sovrascriverlo”.

class BaseClass
{
  public virtual string MetodoVirtuale()
  {
  }
}

In C# le funzioni non sono virtual di default, ma devono essere esplicitamente definite come tali. Si richiede, però, che quando nella classe derivata si esegue una reimplementazione del metodo venga specificato la parola override.

class DerivedClass: BaseClass
{
  public override string MetodoVirtuale()
  {
    ..
  }
}

E' possibile anche omettere la parola override: in tale caso tutto funzionerà nello stesso modo ma si avrà, in fase di compilazione, un warning di avvettimento.

Inoltre se nella classe derivata si scrive un metodo specificando la parola override, ma non esiste alcuna funzione virtual nella classe base con uguale firma, si otterrà una errore.

Quanto esposto sopra viene identificato con il termine metod override, e la sua utilità è esposta nei due casi nel seguito.

Caso 01

Nella classe base si immagini di scrivere un metodo che quasi sicuramente sarà riscritto nella classe derivata.

E’ però assolutamente indispensabile che nella classe derivata, se si vuole riscrivere il metodo, questo sia riscritto in modo corretto cioè conservando la stessa firma di quanto nella classe base.

In atri termini ...se deve essere fatto allora deve essere fatto bene, nascondendo quindi sicuramente il metodo della classe base.

Quindi per per avere l'assoluta certezza che il metodo sia sovrascritto in modo corretto allora sarà necessario marcare il metodo nella classe base come virtual.

Nella classe derivata grazie a questo gli errori potranno essere minimi.

Se si intende sovrascrivere il metodo e si usa, come si dovrebbe, l'istruzione override per confermare questa riscrittura, e per qualche ragione non esiste il corrispondente metodo nella classe base corredato con l'istruzione virtual, si otterrà un errore (l'istruzione override vuole per forza un metodo virtual da "sovrascrivere" altrimenti si avrà un errore).

Se si sovrascrive correttamente il metodo nella classe derivata, senza però usare l'istruzione override, allora si otterranno dei noiosi warning di avvertimento in fase di compilazione.

Caso 02

Nella classe base si aggiungono dei metodi, e uno di questi, per sbaglio, ha la stessa firma di un metodo di una classe derivata.

Normalmente questo non è un grosso problema: verrà sempre richiamato il metodo della classe derivata, ad eccezione di alcuni casi.

Un caso si è visto sopra, ed è quello in cui la variabile dichiarata come classe base viene instanziata usando il new sulla classe derivata.

Un'altro caso, ugualmente insidioso, è il seguente.

public class BaseClass
{
  public virtual void MetodoConOverRide()
  {
    Console.WriteLine("Esecuzione da BaseClass");
  }
  
public virtual void MetodoSenzaOverRide() { Console.WriteLine("Esecuzione da BaseClass");
}
} public class DerivedClass : BaseClass { public override void MetodoConOverRide() { Console.WriteLine("Esecuzione da DerivedClass");
} public void MetodoSenzaOverRide() { Console.WriteLine("Esecuzione da erived");
} }

Data la situazione sopra si immagini di usare gli oggetti come nel seguito.

BaseClass[] basearray = new BaseClass[2];
basearray[0] = new BaseClass();
basearray[1] = new DerivedClass(); for (int i = 0; i < 2; i++)
{
Console.WriteLine("i: " + i.ToString());
basearray[i].MetodoConOverRide();
basearray[i].MetodoSenzaOverRide();
} .. > i: 0
> Esecuzione da BaseClass > Esecuzione da BaseClass
> i: 1
> Esecuzione da DerivedClass
> Esecuzione da BaseClass

Quanto avvenuto nell'iterazione con i = 1, cioè con l'istanza della DerivedClass, può sembrare confuso, ma ha una sua logica solida e fondata: richiamando il metodo MetodoConOverRide viene richiamato il codice implementativo del metodo della classe derivata.

Questo comportamento è causato dal fatto che il metodo della classe base è stato marcato come virtual, e quindi correttamente è oggetto di override nella classe derivata.

Richiamando MetodoSenzaOverRide viene invece richiamato richiamato il relativo metodo della classe base e il comportamento è giustificato dal fatto che la funzione, pur essendo marcata come virtual, non ha il relativo override nella classe derivata.

Come detto precedentemente un tale caso viene comunque segnalato con un warning bello chiaro in fase di compilazione.

I casi visti sopra sono sono abbastanza comuni nella pratica sopratutto l'uso di una matrice o una lista riferita a una classe base per contenere istanze di classi derivate. Occorre evidenziare che spesso la classe base può non essere disponibile (per esempio è una libreria esterna di cui non abbiamo i sorgenti) e questo aumenta la possibilità di incorrere in casi particolari come quelli sopra.

Per questi motivi è sempre meglio essere a conoscenza di eventuali metodi riscritti per analizzarne gli effetti: proprio per questo motivo un metodo con uguale firma nella classe base e derivata, ma non marcati come virtual e override, avranno l'effetto di provocare dei warning di avviso in fase di compilazione.

Metod Hiding

Se un metodo con la stessa firma è dichiarato nella classe base e nella classe derivata, ma i metodi non sono dichiarati rispettivamente come virtual e override, allora il metodo della classe derivata esegue il metod hide del relativo metodo della classe base.

public class BaseClass
{
  public void MetodoDaNascondere()
  {
    Console.WriteLine("Esecuzione da BaseClass");
  }
} public class DerivedClass : BaseClass { public void MetodoDaNascondere() { Console.WriteLine("Esecuzione da DerivedClass");
} }

In questo caso il comportamento è più lineare rispoetto a quanto visto precedentemente.

Base b = new Derived();
b.MetodoDaNascondere();
Derived d = new Derived();
d.MetodoDaNascondere();

> Esecuzione da BaseClass
> Esecuzione da DerivedClass

L'osservazione da evidenziare qui è che il metodo MetodoDaNascondere potrebbe essere stato aggiunto nella classe derivata per disattenzione o comunque non in modo oculato: per far prendere coscenza il programmatore circa la presenza di questo metod hide il compilatore emetterà ampi warning.

Per eliminare questi warning è possibile usare new.

public class BaseClass
{
  public void MetodoDaNascondere()
  {
    Console.WriteLine("Esecuzione da BaseClass");
  }
} public class DerivedClass : BaseClass { public new void MetodoDaNascondere() { Console.WriteLine("Esecuzione da DerivedClass");
} }

Usando l'istruzione new si vuole dire: "ok, ho capito che ho fatto un metod hide, mi sta bene così ora non mi scocciare più e se capitano rogne sono fatti miei".

Che dire ?

Senza tanti giri di parole penso che le due funzionalità esposta sopra (metod override e metod hiding) portino una gran confusione.

Infatti implementano funzionalità simili e questo porta indubbiamente a difficoltà nel comprendere quando usare un sistema e quando invece usare l'altro.

Ma perchè questa sovrapposizione di funzionalità ?

Per cominciare occorre dire che in molto casi non è possibile mettere mano alla classe base, e quindi è giocoforza affidarsi solo al metod hide con eventualmente l'uso del new.

L'altra motivazione sta nel fatto che il C# nasce anche come sostituto/evoluzione di altri linguaggi, e quindi per favorire la migrazione di sviluppatori e codici a questo deve essere in grado di proporre costrutti simili a quello dei linguaggi di origine.

Per tale motivo a seconda del linguaggio da cui si proviene si sarà più portati ad usare il costrutto virtua/-override oppure il method hinding magari con l'uso del new.

Quindi questa ridondanza non deve essere vista come un difetto, bensì di una ricchezza: ci sono due metodi e ogni sviluppatore può scegliere quello che più le appare congeniale.

E' possibile però dare regole generali per sfruttare al meglio queste due possibilità.

Se si progetta una classe base, e uno o più metodi di questa sicuramente saranno reimplementati nelle classi derivate, allora è più sensato marcare questi come virtual, in modo da essere certi che tale riscrittura avvenga in modo efficace e senza errori.

Nelle classe derivate occorrerà ovviamente usare il relativo override, per sbarazzarsi dei warning in fase di compilazione.

Anche se si prevedono di usare matrici o liste che contengano oggetti difformi tutti derivati da una classe base, allora è meglio usare il method override per ottenere un comportamento più sensato.

In tutti gli altri casi occorre operare con new e il method hiding.