1. Introduzione
Immagina questa situazione: a casa hai una TV, un impianto stereo, un condizionatore e una lampadina smart. E ognuno di questi ha il suo telecomando. Per accendere la TV, prendi il telecomando della TV e premi il tasto "Accendi". Per accendere lo stereo, prendi il telecomando dello stereo e premi lo stesso tasto "Accendi". Immaginato? Un casino, vero?
E se invece avessi un telecomando universale? Lo prendi, premi il tasto "Accendi", e lui in qualche modo magico capisce quale dispositivo deve accendere e gli manda il comando giusto. E il telecomando stesso non sa come esattamente si accende la TV o come esattamente si accende lo stereo. Sa solo che tutti questi dispositivi hanno una "funzione di accensione" comune.
Ecco, questa è proprio l'analogia con il polimorfismo nella programmazione!
Polimorfismo
La parola "polimorfismo" viene dal greco poly (molti) e morph (forma). Letteralmente significa "molte forme". Nel contesto dell'OOP è la capacità di un oggetto di assumere forme diverse o, più precisamente, la capacità di uno stesso metodo di comportarsi in modo diverso a seconda del tipo di oggetto su cui viene chiamato.
In C# il polimorfismo si ottiene principalmente grazie all'ereditarietà e all'uso dei metodi virtual e override.
L'idea chiave del polimorfismo è il upcasting (promozione verso l'alto). Che significa?
Torniamo alla nostra gerarchia Animal → Dog, Cat. Sappiamo che un cane è un animale. Un gatto è un animale. Questa relazione "is-a" — è la base fondamentale dell'ereditarietà.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Un certo suono...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Bau-bau!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Miao!");
}
}
Grazie alla relazione "is-a", possiamo assegnare un oggetto di una classe derivata a una variabile del tipo della classe base. Questo si chiama upcasting (promozione verso l'alto), perché stiamo "alzando" l'oggetto a un tipo più generale, quello base. Il compilatore C# lo permette in automatico:
// Abbiamo un cane specifico
Dog myDog = new Dog();
// Possiamo assegnare l'oggetto Dog a una variabile di tipo Animal!
// Questo è proprio l'upcasting.
// A destra - un Dog concreto, a sinistra - un Animal più generale.
Animal generalAnimal = myDog;
// E possiamo chiamare MakeSound()!
generalAnimal.MakeSound(); // Stampa "Bau-bau!" (Non "Un certo suono...", ma proprio "Bau-bau!")
Cosa è successo qui?
- Quando abbiamo scritto Animal generalAnimal = myDog;, non abbiamo creato un nuovo animale. Abbiamo solo preso l'oggetto myDog (che in realtà è un Dog) e l'abbiamo messo in una "scatola" etichettata come Animal.
- In fase di compilazione (quando il codice viene trasformato in bytecode), la variabile generalAnimal ha tipo Animal. Quindi il compilatore "sa" che può chiamare solo i metodi e le proprietà che esistono nella classe Animal.
- Ma la cosa interessante è questa: quando si arriva all'esecuzione del programma (in fase di runtime), e chiamiamo generalAnimal.MakeSound(), il runtime .NET guarda non al tipo della variabile (Animal), ma al tipo reale dell'oggetto che contiene (Dog)! E siccome il metodo MakeSound() è stato segnato come virtual in Animal e override in Dog, viene chiamata proprio l'implementazione di Dog.
Questa magia, quando il comportamento del metodo viene deciso dal tipo reale dell'oggetto a runtime e non dal tipo della variabile, si chiama dynamic dispatch o polimorfismo a runtime.
Immagina una scatola: fuori c'è scritto "Frutto" (il nostro tipo variabile Animal). Dentro ci metti una mela (il nostro oggetto Dog). Quando chiedi "Frutto, fai un suono!" (chiami MakeSound()), la scatola, sapendo che dentro c'è una mela, fa "crunch", non un generico suono "da frutto".
2. Dimostrazione del polimorfismo in azione
Il modo migliore per capire il polimorfismo è vederlo in azione, soprattutto quando hai non uno, ma un intero gruppo di oggetti.
Estendiamo la nostra app "Il mio piccolo zoo". Immagina di lavorare in una clinica veterinaria, e ti portano diversi animali per una visita. Vogliamo che ogni visita segua le sue regole, ma che il codice per chiamare la visita sia universale.
Per prima cosa aggiungiamo al nostro base class Animal e alle sue derivate Dog e Cat un nuovo metodo virtuale Examine():
public class Animal
{
public string Name { get; set; }
public Animal(string name) { Name = name; }
public virtual void MakeSound() { Console.WriteLine($"{Name} fa un certo suono..."); }
public virtual void Examine()
{
Console.WriteLine($"Visita di {Name}:");
Console.WriteLine(" - Controllo della respirazione.");
Console.WriteLine(" - Misurazione della temperatura.");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound() { Console.WriteLine($"{Name} dice: Bau!"); }
public override void Examine()
{
base.Examine();
Console.WriteLine(" - Controllo dei denti.");
Console.WriteLine(" - Vaccino contro la rabbia.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound() { Console.WriteLine($"{Name} dice: Miao!"); }
public override void Examine()
{
base.Examine();
Console.WriteLine(" - Controllo delle unghie.");
Console.WriteLine(" - Pillola contro i vermi.");
}
}
Ora abbiamo metodi Examine() specializzati per cani e gatti. Nota il base.Examine(); nei metodi override. Questo ci permette di eseguire prima i passi comuni della visita (dal base class Animal), e poi aggiungere quelli specifici per ogni tipo di animale. Comodissimo!
Ora immaginiamo la nostra clinica veterinaria. Abbiamo una lista di animali arrivati per la visita:
class Program
{
static void Main()
{
Animal[] animals = {
new Dog("Rex"),
new Cat("Micia"),
new Animal("Coniglio")
};
Console.WriteLine("--- Visita degli animali ---");
foreach (Animal animal in animals)
{
animal.Examine();
Console.WriteLine();
}
Console.WriteLine("--- Ora delle voci ---");
foreach (Animal animal in animals)
animal.MakeSound();
}
}
Risultato:
--- Visita degli animali ---
Visita di Rex:
- Controllo della respirazione.
- Misurazione della temperatura.
- Controllo dei denti.
- Vaccino contro la rabbia.
Visita di Micia:
- Controllo della respirazione.
- Misurazione della temperatura.
- Controllo delle unghie.
- Pillola contro i vermi.
Visita di Coniglio:
- Controllo della respirazione.
- Misurazione della temperatura.
--- Ora delle voci ---
Rex dice: Bau!
Micia dice: Miao!
Coniglio fa un certo suono...
Ecco la potenza del polimorfismo! Abbiamo scritto lo stesso ciclo foreach (Animal animal in animals), e a ogni iterazione abbiamo chiamato lo stesso metodo animal.Examine() (o animal.MakeSound()). Ma ogni volta veniva eseguita la giusta, specifica implementazione di quel metodo per il cane, il gatto o semplicemente l'animale generico. Il codice resta semplice, pulito e universale, e i dettagli sono nascosti dentro ogni classe.
Se domani ci portano un criceto Hamster, basta creare la classe Hamster : Animal, fare l'override di Examine(), e funzionerà nel nostro sistema di visite veterinarie senza cambiare nulla nel ciclo foreach! Questa è la vera forza del polimorfismo.
3. Il ruolo del polimorfismo nell'OOP
Il polimorfismo non è solo una parola figa o un trucchetto di codice. È un principio fondamentale che rende i tuoi programmi flessibili, estendibili e facili da mantenere. Vediamo che ruolo gioca:
Flessibilità ed estendibilità (Open/Closed Principle)
Questo è forse il vantaggio principale. Il polimorfismo ti permette di scrivere codice che è aperto all'estensione ma chiuso alla modifica. Che significa?
- Aperto all'estensione: Puoi aggiungere nuovi tipi di animali (tipo Bird, Fish, Hamster), semplicemente creando nuove classi che ereditano da Animal e fanno l'override dei metodi necessari.
- Chiuso alla modifica: Non devi cambiare il codice esistente che lavora con Animal[]. Il ciclo foreach che chiama animal.Examine() resta identico, non importa quanti nuovi tipi di animali aggiungi.
Immagina quanti if-else if o switch dovresti scrivere e aggiornare continuamente se non ci fosse il polimorfismo! Sarebbe un vero incubo.
Semplificazione del codice
Il polimorfismo semplifica tantissimo il codice che lavora con collezioni di oggetti diversi ma correlati. Invece di controllare il tipo di ogni oggetto e poi chiamare il metodo giusto, chiami semplicemente il metodo comune della classe base, e il sistema decide da solo quale implementazione chiamare.
È come avere un solo pulsante "Apri" per porte diverse: normale, scorrevole, girevole. Non devi ogni volta tirare, spingere o ruotare — basta "aprire", e la porta fa il suo.
Astrattezza
Il polimorfismo è strettamente legato all'astrazione, un altro pilastro dell'OOP di cui parleremo meglio nelle prossime lezioni. Ti permette di concentrarti su "cosa" fa un oggetto (tipo "fa un suono", "viene visitato") invece di "come" lo fa. Lavori con l'idea astratta di "animale", non con il cane o il gatto specifico. Così puoi scrivere codice di alto livello, pulito, che non dipende dai dettagli bassi dell'implementazione.
Riutilizzo del codice
Anche se l'ereditarietà di per sé permette il riutilizzo del codice tramite proprietà e metodi della classe base, il polimorfismo lo potenzia. Ti permette di creare algoritmi e strutture dati universali (tipo Animal[]), che possono lavorare con qualsiasi oggetto ereditato dalla classe base, senza duplicare la logica per ogni tipo derivato.
In sostanza, il polimorfismo rende il tuo codice più "professionale" e pronto ai cambiamenti, cosa fondamentale nello sviluppo reale dove i requisiti cambiano sempre.
4. Dettagli utili
Schema del polimorfismo
classDiagram
class Animal {
+MakeSound()
}
class Dog {
+MakeSound()
}
class Cat {
+MakeSound()
}
class Parrot {
+MakeSound()
}
Animal <|-- Dog
Animal <|-- Cat
Animal <|-- Parrot
Quando chiami MakeSound() tramite un riferimento di tipo Animal — verrà chiamato il metodo della classe dell'oggetto che realmente sta nella variabile.
Uso del polimorfismo con array e collezioni
Un compito molto comune è scorrere una collezione di entità diverse e "lanciare" su tutte la stessa logica.
// Nel nostro programma di allenamento:
Animal[] zoo = { new Dog("Sharik"), new Cat("Barsik"), new Parrot("Kesha") };
foreach (Animal animal in zoo)
{
animal.MakeSound();
}
Nei progetti reali così si fa, ad esempio, la gestione degli eventi, il disegno a schermo (ogni figura a modo suo), la gestione dei pagamenti (tipi diversi di carte e servizi).
Come funziona il polimorfismo
| Tipo oggetto | Tipo variabile | Quale metodo viene chiamato? |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
La cosa fondamentale — il metodo deve essere virtual o abstract nella classe base e override nella derivata!
5. Errori tipici col polimorfismo
Nella vita reale agli studenti capitano spesso questi errori:
- Non rendi il metodo virtual nella classe base — e invece del polimorfismo ottieni sempre lo stesso comportamento.
- Ti confondi: quale classe serve per la variabile? Ricorda sempre: la variabile ha un tipo (ad esempio, Animal), e l'oggetto che ci metti dentro è un istanza (ad esempio, new Dog()).
- Provi a usare nuovi membri che non esistono nella classe base tramite una variabile di tipo base. Ad esempio:
Animal pet = new Dog();
pet.Bark(); // Errore! In Animal non c'è Bark()
Come gestirlo? Se proprio serve, usa il cast, ma cerca di strutturare il codice in modo da lavorare solo con i metodi che esistono nella classe base.
GO TO FULL VERSION