Menu

Ereditarietà in C#: base, virtual, override, sealed e polimorfismo

Come funziona l'ereditarietà tra classi in C#: derivare con i due punti, cosa viene ereditato e cosa no, chiamare la classe base con base, virtual e override, nascondere con new (e l'output che sorprende tutti), sealed, il polimorfismo e i cast verso l'alto e verso il basso nella gerarchia.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

L'ereditarietà permette a una classe di costruire su un'altra. La classe derivata riceve i membri della classe base, aggiunge i propri e può sostituire il comportamento che la classe base le permette di sostituire. Insieme ai metodi virtual, ti dà il polimorfismo: il codice scritto per la classe base esegue il comportamento derivato giusto senza sapere quale classe derivata ha davanti.

Derivare una classe

Metti la classe base dopo i due punti. La classe derivata ha tutto quello che ha la classe base, più quello che dichiara:

Output:

TX-19: 20 km, 2 fares
True

Cosa riceve una classe derivata e cosa no:

  • Ereditato: campi, proprietà, metodi, eventi e tipi annidati. Tutto è fisicamente presente nell'oggetto.
  • Accessibile: solo ciò che la base permette: i membri public, protected e internal. Un membro private di Vehicle esiste dentro ogni Taxi, ma il codice di Taxi non può nominarlo. Ecco perché Km ha un private set: Taxi può leggerlo, e lo modifica solo tramite Drive.
  • Non ereditato: i costruttori. Taxi deve dichiarare i propri e concatenarsi a uno di quelli di Vehicle con : base(plate). La pagina sui costruttori mostra in che ordine vengono eseguiti i due.

Ogni classe deriva in ultima analisi da object, ed è per questo che ogni oggetto ha ToString(), Equals() e GetHashCode().

virtual e override

Una classe base marca un metodo come virtual per dire "le classi derivate possono fornire una loro versione". Una classe derivata lo sostituisce con override. Dentro l'override, base.Method() chiama la versione della classe base.

Output:

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

La variabile del ciclo è di tipo Notification, eppure ogni oggetto si visualizza a modo suo. Questo è il polimorfismo: la chiamata n.Render(...) viene risolta a runtime in base al tipo effettivo dell'oggetto. Nota anche che base.Render in Email usa Channel, che è a sua volta virtuale, quindi il metodo base stampa email, non generic. Una chiamata virtuale dentro la classe base raggiunge comunque l'override.

Anche le proprietà possono essere virtuali, come mostra Channel. I campi no.

Il compilatore ti obbliga a usare entrambe le parole chiave. Scrivere override su un metodo che non è virtual dà l'errore CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). Omettere override quando il metodo base è virtuale è solo un avviso, e cambia completamente il significato, come mostra la prossima sezione.

new e override: nascondere invece di sovrascrivere

Se una classe derivata dichiara un metodo con la stessa firma di un metodo base senza scrivere override, nasconde il metodo base. Il compilatore dà un avviso (CS0114 per un metodo base virtuale, CS0108 negli altri casi) e suggerisce la parola chiave new, che zittisce l'avviso ma mantiene il comportamento di occultamento:

Output:

Sales report
Report
Draft report

b e c sono lo stesso tipo di oggetto, eppure stampano titoli diversi. Con new, il metodo scelto dipende dal tipo della variabile, deciso in fase di compilazione. Il codice che gestisce i report come Report (una lista, un parametro di un metodo, una callback del framework) non vede mai DraftReport.Title. Non è quasi mai quello che vuoi. Usa override per il polimorfismo; new esiste soprattutto per il caso in cui una classe base che non controlli aggiunge un membro il cui nome entra in conflitto con uno che hai già.

sealed

sealed su una classe vieta di derivare da essa:

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string è sealed, come molti tipi del framework. Su un override, sealed ferma la catena a quel livello:

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

Progettare una classe per l'ereditarietà richiede lavoro: decidere cosa è virtuale, su cosa possono contare le classi derivate, in che ordine succedono le cose. Una classe che non è stata progettata così è più sicura se sigillata, e la sigillatura si può togliere in seguito senza rompere niente a nessuno, mentre una volta che altri derivano dalla tua classe non puoi più tornare indietro. Anche le chiamate ai membri delle classi sealed possono essere leggermente più veloci, perché il runtime sa che non esiste nessun override.

Una classe base, molte interfacce

Una classe C# ha esattamente una classe base. class Admin : User, Employee dà l'errore CS1721 ("cannot have multiple base classes"). Una classe può però implementare un numero qualsiasi di interfacce oltre alla sua classe base:

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

Usa una classe base per "è un tipo di, e ne condivide l'implementazione", e le interfacce per "sa fare". Quando la classe base esiste solo per obbligare le classi derivate a completare alcuni metodi, lo strumento giusto è una classe astratta.

Cast verso l'alto e verso il basso nella gerarchia

Un oggetto derivato si può sempre usare dove è previsto il suo tipo base. Questo upcast è implicito e non può fallire. Nell'altra direzione, un downcast richiede un cast esplicito e fallisce a runtime se l'oggetto non è di quel tipo:

Output:

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

Scrivere Dog d = pet; senza cast è un errore di compilazione (CS0266: "An explicit conversion exists (are you missing a cast?)"), perché il compilatore sa solo che pet è un qualche Animal. Preferisci is con una variabile quando l'oggetto potrebbe essere di un altro tipo, e un cast semplice solo quando qualsiasi altro tipo sarebbe un bug. Fare spesso downcast è un campanello d'allarme nel design: di solito significa che il comportamento andrebbe messo in un metodo virtuale della classe base. La pagina sul pattern matching tratta per intero le forme di is.

Ereditarietà e collezioni

Il polimorfismo funziona elemento per elemento, ma le collezioni generiche di un tipo derivato non sono collezioni del tipo base: List<Animal> animals = new List<Dog>(); non compila, perché la lista accetterebbe poi un Cat. Le viste di sola lettura sono covarianti, quindi IEnumerable<Animal> animals = new List<Dog>(); va bene. La pagina sui generics spiega perché.

Errori comuni

  • Dimenticare override. Il metodo compila con un avviso e in silenzio nasconde invece di sovrascrivere. Tratta CS0114 come un errore.
  • Rendere tutto virtual. Ogni membro virtuale è una promessa alle classi derivate su quando viene chiamato e su cosa può dare per scontato. Marca solo i punti di estensione che hai in mente.
  • Gerarchie profonde. Tre o quattro livelli di ereditarietà rendono difficile capire quale versione di un metodo viene eseguita. La composizione, cioè una classe che contiene un altro oggetto e lo chiama, è spesso più semplice di un nuovo livello.
  • Chiamare metodi virtuali da un costruttore. L'override viene eseguito prima del corpo del costruttore derivato, quindi qualsiasi campo assegnato in quel corpo contiene ancora il suo valore predefinito.
  • Usare l'ereditarietà solo per riusare codice. Se uno Stack eredita da List, chi lo usa può fare Insert in mezzo al tuo stack. Tieni invece una List in un campo privato.

Domande frequenti

Come funziona l'ereditarietà in C#?

Una classe indica una classe base dopo i due punti: class Dog : Animal. La classe derivata riceve tutti i membri della classe base (campi, proprietà, metodi, eventi), può aggiungere i propri e può sovrascrivere quelli che la base ha marcato virtual. I costruttori non vengono ereditati, e i membri private, pur essendo presenti nell'oggetto, non sono accessibili dalla classe derivata.

Che differenza c'è tra virtual e override in C#?

virtual va sul metodo della classe base e dice "le classi derivate possono sostituirlo". override va sul metodo della classe derivata ed effettua la sostituzione. Servono entrambi: sovrascrivere un metodo che non è virtual, abstract o già override dà l'errore CS0506. Quando viene chiamato un metodo virtuale, il runtime esegue la versione del tipo effettivo dell'oggetto, non del tipo della variabile.

Che differenza c'è tra new e override in C#?

override sostituisce il metodo base per chiunque lo chiami, anche per il codice che accede all'oggetto tramite una variabile del tipo base. new lo nasconde soltanto: il codice che vede l'oggetto come tipo derivato chiama il nuovo metodo, mentre il codice che lo vede come tipo base chiama ancora il metodo base. Quindi Base b = new Derived(); b.M(); esegue Derived.M con override e Base.M con new.

C# supporta l'ereditarietà multipla?

Non per le classi: una classe ha esattamente una classe base, e indicarne due dà l'errore CS1721. Una classe può implementare un numero qualsiasi di interfacce, ed è così che C# rappresenta "questo tipo sa fare più cose". Da C# 8 le interfacce possono anche contenere implementazioni predefinite dei metodi.

Cosa significa sealed in C#?

Una classe sealed non può essere usata come classe base; derivare da essa dà l'errore CS0509. string, per esempio, è sealed. Su un metodo, sealed override impedisce alle classi più in basso nella gerarchia di sovrascriverlo di nuovo (CS0239). Sigillare le classi che non sono state progettate per l'ereditarietà è una scelta predefinita ragionevole.

Come chiamo il metodo della classe base in C#?

Usa base.MethodName(...) dentro la classe derivata, di solito dentro l'override: public override string Describe() => base.Describe() + " with GPS";. Per i costruttori, usa : base(...) dopo l'elenco dei parametri per scegliere quale costruttore base viene eseguito.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA