back to top

Ereditarietà e interfacce in C#

L’ereditarietà è uno dei concetti chiave nella programmazione OOP. In pratica l’ereditarietà esprime la relazione che collega due classi all’interno di un programma. Nella programmazione a oggetti esistono due tipi di ereditarietà: l’ereditarietà di implementazione e l’ereditarietà di interfaccia.

  • Ereditarietà di implementazione significa che un tipo deriva da un altro tipo base, prendendo tutti i membri e le funzioni del tipo base. In questo modo un tipo derivato adotta l’implementazione delle funzioni del tipo base (a meno che non venga effettuato il cosiddetto override delle funzioni). Questo tipo di ereditarietà è molto utile quando è necessario aggiungere funzionalità ad un tipo esistente oppure quando un certo numero di tipi correlati condividono un significativo numero di funzionalità.
  • Ereditarietà di interfaccia significa che un tipo eredita solo la firma delle funzioni, non l’implementazione. Questo tipo di ereditarietà è molto utile quando si desidera specificare che un tipo deve fornire alcune funzionalità precise senza però voler specificare il contenuto di tali funzionalità.

C# supporta entrambi i tipi di ereditarietà e lascia liberi gli sviluppatori di utilizzare quella più opportuna in base all’architettura delle proprie applicazioni.

Alcuni linguaggi di programmazione consentono la cosiddetta ereditarietà multipla tramite cui è possibile che una classe derivi da più di una classe base. Il linguaggio C# non supporta questo tipo di ereditarietà ma consente ai tipi di essere derivati da più interfacce. Questo significa che una classe in C# può essere derivata da una sola altra classe mentre può derivare da qualsiasi numero di interfacce.

Classi derivate e override

Si ha ereditarietà di implementazione quando una classe deriva da un’altra classe. La sintassi per indicare che una classe deriva da un’altra classe è la seguente

class ClasseDerivata: ClasseBase
{
  // funzioni e membri
}

Se la classe deriva anche da interfacce invece

class ClasseDerivata: ClasseBase, IInterfaccia1, IInterfaccia2
{
  // funzioni e membri
}

Come detto in precedenza è possibile in una classe derivata effettuare l’override di una funzione della classe base, cioè specificare un comportamento diverso da quello della funzione originaria. Per fare questo è necessario ricorrere alla keyword virtual nella classe originale. Vediamo un esempio:

class ClasseBase
{
  public virtual string MetodoVirtuale()
  {
    return "Metodo originale";
  }
}

Se vogliamo specificare un comportamento diverso per il metodo MetodoVirtuale nella classe derivata basta utilizzare la seguente sintassi con il termine override:

class ClasseDerivata: ClasseBase
{
  public override string MetodoVirtuale()
  {
    return "Metodo alternativo";
  }
}

Se un metodo con la stessa firma viene dichiarato sia nella classe base che in quella derivata, ma esso non viene dichiarato rispettivamente come virtual e override, si dice che il metodo della classe derivata "nasconde" quello della classe base. Questa è una pratica rischiosa poiché si potrebbe chiamare un metodo non desiderato confondendo le due implementazioni.

Il linguaggio C# ha una sintassi particolare per richiamare la versione base di un metodo da una classe derivata. Tale sintassi è la seguente

Base.<Nome_Metodo>()

Quindi tornando all’esempio precedente

class ClasseDerivata: ClasseBase
{
  public override string MetodoVirtuale()
  {
    base. MetodoVirtuale();

    //Eventuale altro codice
  }
}

La classe Object

In C# tutte le classi derivano da una singola classe, la classe Object. Molte altre classi a loro volta fungono da classi base per altre classi, creando una gerarchia di ereditarietà. La classe che si trova in cima a tale gerarchia viene detta root class (classe radice).

La classe Object fornisce un certo numero di metodi di "basso livello" che le sottoclassi possono sottoporre ad override. Tra questi il metodo Equals() (che verifica se due oggetti sono uguali) ed il metodo ToString() (che ritorna una stringa corrispondente al nome della classe a cui l’oggetto appartiene).

Classi e metodi sealed

Altra tipologia di classi e metodi sono quelli sealed. Nel caso di una classe significa che non è possibile ereditare da essa, nel caso di un metodo che non è possibile effettuarne l’override. Solitamente le classi vengono marcate come sealed quando contengono solamente metodi e proprietà statici.

Classi e funzioni astratte

E’ possibile dichiarare classi di tipo abstract. Una classe astratta è una classe priva di implementazione e, come tale, non può essere istanziata direttamente. Le classi astratte sono utilizzate come "basi" di partenza su cui sviluppare altre classi (derivate) aventi tra loro in comune la sola interfaccia.

Anche le funzioni possono essere astratte (sono cioè prive di implementazione). Ovviamente una funzione astratta è automaticamente virtuale e se una classe contiene funzioni astratte deve essere di conseguenza dichiarata come astratta:

abstract class  ClasseBase
{
  public abstract string MetodoAstratto();
}

Le interfacce

Le interfacce vengono utilizzate in C# per definire una sorta di "scheletro" che le classi derivate devono rispettare. Per convenzione le interfacce hanno nomi che iniziano con "I".

Non è possibile istanziare un’interfaccia poiché essa contiene solo le firme di metodi, proprietà, eventi o indicizzatori. Per "firme" s’intende che per le proprietà, i metodi e gli eventi sono definiti solo il nome e parametri di input e di output (in pratica non ne è specificato il funzionamento).

Si ha ereditarietà di interfaccia quando una classe implementa un’interfaccia.

Una classe che implementa l’interfaccia:

  • deve implementarne i membri specificati nella definizione dell’interfaccia stessa;
  • non può specificare modificatori sui membri, i qiuali sono implicitamente di tipo public e non possono essere dichiarati come virtual o static.

Una delle interfacce predefinite di Microsoft è la System.IDisposable. Questa interfaccia contiene il metodo Dispose che viene utilizzato dalle classi quando un oggetto non è più necessario al fine di liberare le risorse del sistema. Vediamo un esempio:

public interface IDisposable
{
  void Dispose();
}

Se una classe vuole utilizzare il metodo Dispose deve ereditarlo dall’interfaccia IDisposable nel modo seguente

class UnaClasse: IDisposable
{
  public void Dispose()
  {
    // implementazione del metodo Dispose
  }
  
  // resto della classe

}

E’ opportuno evidenziare che questa classe deve contenere necessariamente un’implementazione del metodo Dispose, in caso contrario, in fase di compilazione, si ottiene un errore.

E’ anche possibile che un’interfaccia derivi da altre interfacce, allo stesso modo delle classi.

Differenze tra Interfacce e Classi Astratte

A questo punto il lettore potrebbe essere confuso e notare una certa identità tra classi astratte ed interfacce. In realtà esistono differenze importanti che è il caso di rimarcare:

  • una classe astratta può avere dei metodi implementati (l’interfaccia no);
  • una classe astratta può avere campi (l’interfaccia no);
  • una classe astratta può ereditare da una interfaccia, viceversa no.
Pubblicitร