1. Introduzione
Quando un principiante sente la parola ereditarietà, può sembrare tutto facile: basta prendere una classe esistente, estenderla o modificarla un po' — fatto! In realtà ci sono un sacco di sfumature. Gli errori nell'ereditarietà possono manifestarsi in modi diversi: firme sbagliate, parole chiave dimenticate, design della gerarchia poco chiaro — tutto questo porta a bug difficili.
A volte gli errori si vedono subito: il codice non compila. Altre volte invece i bug saltano fuori solo a runtime, quando il tuo nuovo SuperMegaLogger scrive tutt'altro rispetto a quello che si aspettano gli utenti, o magari non scrive proprio niente. Tristezza, delusione, ore e ore di debug — ti suona familiare?
Dai, vediamo insieme gli errori più frequenti legati all'ereditarietà e all'override dei metodi, e "curiamo" il nostro codice strada facendo.
2. Dimenticare virtual: perché non si può fare override di un metodo?
Problema
In C# puoi fare override (override) solo dei metodi che nella classe base sono dichiarati con la parola chiave virtual, oppure come abstract, oppure come override (nella catena di ereditarietà). Se il metodo non ha questa parola, provare a scrivere override nella classe derivata ti darà errore di compilazione.
class Animal
{
public void Speak()
{
Console.WriteLine("L'animale dice qualcosa.");
}
}
class Cat : Animal
{
// Errore di compilazione! Il metodo nella classe base non è virtual, abstract o override.
public override void Speak()
{
Console.WriteLine("Miao!");
}
}
L'errore sarà qualcosa tipo: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override".
Come non cascarci?
Per poter fare override dei metodi, dichiarali sempre come virtual nella classe base. Tra l'altro, se stai progettando classi da estendere, pensa sempre a quali metodi potrebbero essere utili da sovrascrivere.
class Animal
{
public virtual void Speak()
{
Console.WriteLine("L'animale emette un suono.");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Miao!");
}
}
Perché serve?
Dichiarando un metodo come virtual, autorizzi esplicitamente i figli a cambiarne il comportamento, e il compilatore ti aiuta — non ti farà fare override del "metodo sbagliato" per sbaglio.
3. Errore con le parole chiave override e new
A volte succede il contrario: chi scrive la classe derivata vuole "sovrascrivere" un metodo, ma nella base non è virtual. Qui il compilatore non ti fa usare override, ma ti lascia usare new. Ma è tutta un'altra storia!
class Dog : Animal
{
// Questo non è override, ma hiding del metodo della classe base.
public new void Speak()
{
Console.WriteLine("Bau!");
}
}
Se chiami questo metodo tramite una variabile di tipo Dog — tutto ok:
Dog dog = new Dog();
dog.Speak(); // "Bau!"
Ma se accedi all'oggetto tramite una variabile di tipo base, viene chiamato il metodo base:
Animal dog2 = new Dog();
dog2.Speak(); // "L'animale emette un suono."
Spiegazione:
La parola chiave new NON fa override del metodo, ma lo nasconde. Si chiama "hiding". Questo può confondere, perché il comportamento polimorfico non funziona come ti aspetti.
Come evitare le trappole di new vs override?
Se vuoi il classico override con polimorfismo — usa virtual/override. Se invece aggiungi una funzionalità completamente nuova (o vuoi nascondere consapevolmente il comportamento del metodo base, ma occhio!), allora usa new.
| Situazione | Parola chiave | Polimorfismo? | Comportamento tramite tipo base |
|---|---|---|---|
| Cambiare comportamento | override | Sì | Viene chiamato il metodo della classe derivata |
| Nascondere/sostituire metodo | new | No | Viene chiamato il metodo della classe base |
4. Firme dei metodi che non coincidono
I principianti, e a volte anche i più esperti, sbagliano cambiando i tipi dei parametri o il tipo di ritorno nel metodo derivato. Per esempio, se il metodo base è public virtual void Print(string message), e nella derivata "sovrascrivono" public override void Print(object message), questo NON è override, ma un nuovo metodo.
class Printer
{
public virtual void Print(string msg)
{
Console.WriteLine("Stampante base: " + msg);
}
}
class SmartPrinter : Printer
{
// Errore di compilazione! La firma non coincide con il metodo base.
public override void Print(object msg)
{
Console.WriteLine("Stampante smart: " + msg);
}
}
Tip:
Devono coincidere nome del metodo, tipo di ritorno e parametri (tipo, numero e ordine).
Se per sbaglio cambi il tipo di un parametro o sbagli il nome — il compilatore ti avvisa.
5. Modificatori di accesso non coerenti
Un altro tranello in cui cadono spesso i principianti — i modificatori di accesso. Il metodo derivato non può essere più restrittivo di quello base. Esempio di codice sbagliato:
public class Vehicle
{
public virtual void StartEngine() { /* ... */ }
}
public class Car : Vehicle
{
// Errore! Il modificatore 'private' è più restrittivo di 'public' nella base.
private override void StartEngine() { /* ... */ }
}
Cosa fare?
Il modificatore di accesso nel metodo derivato deve essere uguale o più aperto rispetto a quello base. Di solito public o protected.
6. Metodo astratto dimenticato/in eccesso
Se nella classe base un metodo è dichiarato come abstract, allora nella derivata DEVI fare override, altrimenti anche la classe derivata diventa astratta (e non puoi istanziarla).
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
// Errore! Metodo astratto Area() non implementato
}
Soluzione:
Devi implementare il metodo:
class Circle : Shape
{
public override double Area()
{
return 3.14 * 2 * 2; // Più o meno...
}
}
7. Chiamare l'implementazione base: base.Method()
A volte non vuoi sostituire completamente l'implementazione di un metodo, ma solo aggiungere qualcosa. In questi casi spesso ci si dimentica (o non si sa) che nel metodo derivato puoi chiamare l'implementazione della classe base con la parola chiave base.
class Logger
{
public virtual void Log(string msg)
{
Console.WriteLine("Log base: " + msg);
}
}
class FancyLogger : Logger
{
public override void Log(string msg)
{
// Puoi aggiungere "feature" e chiamare il metodo base:
Console.WriteLine("[FANCY] " + msg);
base.Log(msg);
}
}
Spiegazione:
Se non chiami base.Log(msg), la logica della classe base viene persa del tutto.
8. Chiamare il costruttore base (base)
Se la classe base richiede parametri obbligatori nel suo costruttore, la classe derivata deve chiamare esplicitamente il costruttore giusto con la parola chiave base.
class Engine
{
public Engine(int cylinders)
{
Console.WriteLine("Motore con cilindri: " + cylinders);
}
}
class RaceEngine : Engine
{
// Errore di compilazione! Engine non ha costruttore di default.
public RaceEngine() { }
}
// Variante corretta:
class RaceEngine2 : Engine
{
public RaceEngine2() : base(8) // Chiamiamo esplicitamente il costruttore base
{
Console.WriteLine("Motore da corsa pronto!");
}
}
9. Dimenticare di segnare i metodi come sealed
A volte vuoi impedire che un metodo venga ulteriormente sovrascritto nelle classi derivate — in C# c'è la parola chiave sealed insieme a override. Se non la usi, qualcuno potrebbe fare override del tuo metodo, magari rompendo la tua logica.
class Hero
{
public virtual void Attack() => Console.WriteLine("L'eroe attacca!");
}
class Warrior : Hero
{
public sealed override void Attack() => Console.WriteLine("Il guerriero colpisce!");
}
class Mutant : Warrior
{
// Errore! Il metodo Attack è sealed sopra.
// public override void Attack() { ... }
}
Spiegazione:
Così "sigilli" l'implementazione a questo livello e le classi sotto non possono più cambiarla.
10. Overload inutili o sbagliati invece di override
A volte si confonde overloading (sovraccarico) di un metodo (overloading) con override (overriding). L'overloading è definire un metodo con lo stesso nome ma firma diversa (ad esempio, numero di parametri diverso), e non c'entra nulla col polimorfismo.
class Animal
{
public virtual void Eat()
{
Console.WriteLine("L'animale mangia.");
}
}
class Panda : Animal
{
// Questo NON è override! Solo un nuovo overload.
public void Eat(string what)
{
Console.WriteLine("Il panda mangia: " + what);
}
}
...
Animal a = new Panda();
a.Eat(); // Se il metodo è overridato — viene chiamata la versione della derivata. Se no — si usa quella base
// a.Eat("bamboo"); // Errore di compilazione: Animal non ha questo metodo
Per aggiungere comportamento polimorfico, devi fare override, non solo overload.
11. Errori di design formali e informali
Quando le classi sono progettate male, le gerarchie diventano un casino:
- una "giostra" interna di override senza usare base. come si deve,
- uso incoerente dei modificatori,
- gerarchie troppo profonde o poco chiare,
- metodi con logica "spalmata" su più livelli di ereditarietà,
- assenza di commenti su metodi virtual/abstract,
- effetti collaterali poco chiari (tipo chiamare un metodo virtuale nel costruttore della base, quando la derivata non è ancora inizializzata del tutto).
Consiglio:
Se ti senti perso — prendi carta e penna (vecchia scuola!) e disegna la gerarchia, scrivi dove sta ogni metodo, chi lo overridea, cosa chiama, e dove va chiamato base..
Nei progetti veri è fondamentale anche documentare cosa ci si aspetta dall'override di ogni metodo.
GO TO FULL VERSION