1. Introduzione
Immagina un mondo senza gerarchie: migliaia di classi Person, Animal, Vehicle e tutto completamente separato. Non c’è da stupirsi che i programmatori in quel mondo non arriverebbero nemmeno a pranzo — si incasinerebbero di brutto! Nei progetti reali spesso ci servono oggetti che sanno fare qualcosa in comune (tipo tutti gli animali possono muoversi), ma ognuno ha le sue particolarità (il pesce nuota, l’uccello vola).
Proprio le gerarchie di classi permettono di esprimere i legami tra entità, così programmare diventa un lavoro creativo e non una lotta infinita contro il copia-incolla.
Vediamo un esempio. Supponiamo di avere una classe base Animal. Tutti gli animali possono fare un suono. Ma solo i gatti miagolano, i cani abbaiano e i pappagalli possono pure raccontare qualche barzelletta. Vogliamo esprimere tutto questo nel codice in modo gerarchico.
Classe base
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Metodo base: può essere sovrascritto nei figli
public virtual void Speak()
{
Console.WriteLine("L’animale fa qualche suono...");
}
}
Qui abbiamo aggiunto la parola virtual al metodo Speak(). È come dire: "Ehi, classi figlie, se volete — potete sovrascrivere questo metodo".
Creiamo la gerarchia: classi derivate
Ora facciamo una classe Cat, che eredita da Animal:
public class Cat : Animal
{
public Cat(string name) : base(name) { }
// Sovrascriviamo Speak — i gatti non possono solo ringhiare!
public override void Speak()
{
Console.WriteLine($"{Name} dice: Miao!");
}
}
E la classe Dog:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dice: Bau!");
}
}
E se abbiamo un animale normale che non sa parlare? Allora possiamo usare la classe base senza sovrascrivere nulla.
Visualizzazione — albero della gerarchia
. Animal
/ \
Cat Dog
- Animal — classe base
- Cat, Dog — classi figlie (derivate)
2. Scriviamo del codice che usa questa gerarchia
Continuiamo a lavorare sulla nostra app da console.
Supponiamo di avere una collezione di animali e vogliamo che ognuno dica qualcosa di caratteristico:
Animal[] zoo = new Animal[]
{
new Cat("Barsik"),
new Dog("Rex"),
new Animal("Creatura misteriosa")
};
foreach (Animal animal in zoo)
{
animal.Speak();
}
Output atteso:
Barsik dice: Miao!
Rex dice: Bau!
L’animale fa qualche suono...
Così, grazie alla gerarchia e al polimorfismo (tra poco lo vediamo meglio, ma in pratica — viene chiamata la versione giusta del metodo a seconda del tipo reale dell’oggetto), la tua app diventa flessibile ed estendibile.
3. Aggiungiamo nuovi metodi e campi
Ok, con i “versi” ci siamo. Ma tutti gli animali così sono troppo noiosi. Per esempio, un gatto può avere nove vite, e un cane sa riportare il bastone.
Aggiungiamo comportamenti unici
Nella classe figlia puoi aggiungere i tuoi metodi e campi:
public class Cat : Animal
{
public int Lives { get; private set; } = 9;
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dice: Miao! Ho {Lives} vite.");
}
public void LoseLife()
{
if (Lives > 0)
{
Lives--;
Console.WriteLine($"{Name} ha perso una vita. Rimaste: {Lives}");
}
else
{
Console.WriteLine($"{Name} ha già usato tutte le vite!");
}
}
}
Usiamolo nel codice:
var barsik = new Cat("Barsik");
barsik.Speak(); // Barsik dice: Miao! Ho 9 vite.
barsik.LoseLife(); // Barsik ha perso una vita. Rimaste: 8
Aggiungiamo nuove classi: allarghiamo lo "zoo"
Ormai sai creare classi derivate. Aggiungiamo, per esempio, un pappagallo:
public class Parrot : Animal
{
public Parrot(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dice: Ciao, umano!");
}
public void Repeat(string phrase)
{
Console.WriteLine($"{Name} ripete: {phrase}");
}
}
Ora puoi estendere il sistema senza toccare il vecchio codice:
var keshka = new Parrot("Kesha");
keshka.Speak(); // Kesha dice: Ciao, umano!
keshka.Repeat("Impara, studente!"); // Kesha ripete: Impara, studente!
4. Confronto dei comportamenti degli animali
| Tipo | Metodo Speak() | Campo proprio | Comportamento aggiuntivo |
|---|---|---|---|
| Animal | Sì (virtual) | Name | — |
| Cat | Sì (override) | Lives | LoseLife() |
| Dog | Sì (override) | — | — |
| Parrot | Sì (override) | — | Repeat(string) |
Come appare la gerarchia delle classi in memoria (schema a blocchi)
Animal (Name)
├── Cat (Lives)
├── Dog
└── Parrot (Repeat)
5. Pratica nella nostra app
Colleghiamo l’idea di gerarchia all’app — per esempio, abbiamo task di tipo diverso:
- Task (classe base): Qualsiasi task — ha un titolo e uno stato di completamento.
- WorkTask (di lavoro): Oltre a tutto il resto, ha una deadline.
- HomeTask (di casa): Può avere una priorità ("Molto importante", "Così così").
Partiamo dalla classe base:
public class Task
{
public string Title { get; set; }
public bool IsCompleted { get; private set; }
public Task(string title)
{
Title = title;
}
public virtual void Complete()
{
IsCompleted = true;
Console.WriteLine($"Task \"{Title}\" completato!");
}
}
Ora aggiungiamo il task di lavoro:
public class WorkTask : Task
{
public DateTime Deadline { get; set; }
public WorkTask(string title, DateTime deadline)
: base(title)
{
Deadline = deadline;
}
public override void Complete()
{
base.Complete();
Console.WriteLine($"Scadenza: {Deadline:d}");
}
}
E il task di casa:
public class HomeTask : Task
{
public string Priority { get; set; }
public HomeTask(string title, string priority)
: base(title)
{
Priority = priority;
}
// Non serve sovrascrivere Complete se il comportamento della base va bene
}
Facciamo una lista di task nell’app:
List<Task> tasks = new List<Task>
{
new WorkTask("Inviare il report", DateTime.Today.AddDays(2)),
new HomeTask("Lavare i piatti", "Molto importante"),
new Task("Leggere la lezione sull’ereditarietà")
};
foreach (Task task in tasks)
{
Console.WriteLine($"Task: {task.Title}");
task.Complete();
}
Output atteso:
Task: Inviare il report
Task "Inviare il report" completato!
Scadenza: 13.07.2025
Task: Lavare i piatti
Task "Lavare i piatti" completato!
Task: Leggere la lezione sull’ereditarietà
Task "Leggere la lezione sull’ereditarietà" completato!
Vedi che comodo: tutti i task stanno insieme, li gestiamo allo stesso modo, e le particolarità saltano fuori solo dove servono.
6. Errori tipici nell’uso dell’ereditarietà
Errore n°1: tentare di sovrascrivere un metodo che non è dichiarato virtual.
Se il metodo nella classe base non è marcato come virtual, non puoi sovrascriverlo nelle derivate. Così perdi tutta la flessibilità del polimorfismo e la gerarchia diventa inutile.
Errore n°2: ereditarietà senza un legame logico tra le entità.
Non usare l’ereditarietà se gli oggetti non sono collegati di senso. Per esempio, Cerchio è davvero una Figura, ma Cavallo come Mezzo di trasporto — è un po’ forzato. Fa eccezione un contesto specifico (tipo un gioco medievale), dove il legame può avere senso.
Errore n°3: gerarchie troppo profonde.
Quando la struttura delle classi va troppo in profondità (5–6 livelli o più), il codice diventa difficile da leggere, mantenere e testare. È un segnale che forse dovresti pensare alla composizione invece dell’ereditarietà.
Errore n°4: dimenticare di chiamare il costruttore base.
Quando aggiungi nuove proprietà nella classe derivata è facile dimenticare di chiamare esplicitamente base(...) nel costruttore. Questo può portare a inizializzazioni incomplete o sbagliate della parte base dell’oggetto e a bug difficili da trovare.
GO TO FULL VERSION