1. Introduzione
Torniamo al nostro zoo. Abbiamo una classe base Animal (Animale) e le sue derivate: Dog (Cane), Cat (Gatto), Fish (Pesce).
Nelle lezioni precedenti abbiamo aggiunto alla classe Animal un metodo virtual MakeSound():
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public virtual void MakeSound() // Metodo virtual
{
Console.WriteLine("Qualche animale emette un suono."); // Implementazione di default
}
public void Sleep() // Metodo normale
{
Console.WriteLine($"{Name} dorme.");
}
}
public class Dog : Animal
{
public override void MakeSound() // Sovrascriviamo il suono per il cane
{
Console.WriteLine("Bau-bau!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Sovrascriviamo il suono per il gatto
{
Console.WriteLine("Miao!");
}
}
Funziona alla grande! Quando creiamo un Dog o un Cat e chiamiamo MakeSound(), sentiamo i loro suoni unici. Ma cosa succede se creiamo semplicemente un Animal?
Animal genericAnimal = new Animal();
genericAnimal.Name = "Creatura sconosciuta";
genericAnimal.MakeSound(); // Stamperà: "Qualche animale emette un suono."
Sembra logico. Ma a volte capita che l'implementazione di default nella classe base non abbia proprio senso. E se Animal non fosse un animale concreto, ma piuttosto un concetto? "Animale" di per sé non emette un suono specifico, lo fanno già le specie concrete. Oppure, immagina di avere una classe Shape (Figura) con un metodo CalculateArea() (CalcolaArea). Che area dovrebbe calcolare una semplice Figura? Nessuna! L'area ce l'ha il Cerchio, il Quadrato, ma non la "Figura" astratta.
In questi casi, quando la classe base non può (o non deve) fornire un'implementazione di default sensata, ma obbliga comunque tutte le sue derivate a implementare quel metodo, ci vengono in aiuto i metodi astratti e le classi astratte.
2. Classi astratte: quando il progetto non è ancora una casa
Immagina di essere un architetto e di creare il progetto di una casa tipo. Ma non di una casa qualsiasi, bensì di una "Casa Concettuale". Ha delle caratteristiche comuni: muri, tetto, fondamenta. Ma ancora non sai se sarà un cottage a un piano o un grattacielo. Alcune parti del progetto saranno concrete (ad esempio, l'altezza dei soffitti al primo piano), altre invece solo accennate (tipo "numero di piani", che sarà deciso dopo).
Nel mondo C#, questa "Casa Concettuale" si chiama classe astratta.
Classe astratta — è una classe marcata con la parola chiave abstract.
public abstract class Animal // Ora Animal è una classe astratta
{
// ...
}
Caratteristica chiave delle classi astratte:
- Non puoi crearle direttamente. Non puoi scrivere new Animal(). Perché? Perché Animal ora è qualcosa di indefinito, un concetto. Non puoi costruire una "Casa Concettuale", puoi solo costruire un cottage concreto o un grattacielo.
Se provi a scrivere new Animal(), il compilatore ti blocca subito:
Questa è una limitazione molto importante!Cannot create an instance of the abstract type or interface 'Animal' (Impossibile creare un'istanza del tipo astratto o dell'interfaccia 'Animal') - Possono contenere membri astratti. Ed è qui che viene il bello!
3. Metodi astratti: contratto senza implementazione
Se la classe astratta è la "Casa Concettuale", allora il metodo astratto è quella parte del progetto dove c'è scritto "fare questo", ma senza istruzioni precise su "come". Tipo "Costruire le fondamenta" — è una parte obbligatoria della casa, ma le dimensioni e i materiali dipendono dal tipo di casa.
Un metodo astratto è un metodo che:
- È marcato con la parola chiave abstract.
- Non ha corpo (cioè, niente blocco di codice {}). Finisce con il punto e virgola ;.
- Può essere dichiarato solo dentro una classe astratta.
Facciamo diventare il nostro metodo MakeSound() astratto:
public abstract class Animal // Abbiamo reso Animal astratta
{
public string Name { get; set; }
public int Age { get; set; }
public abstract void MakeSound(); // Ecco il metodo astratto! Niente corpo!
public void Sleep() // Questo metodo resta normale, "concreto"
{
Console.WriteLine($"{Name} dorme.");
}
}
Guarda come è cambiato MakeSound()! Non ha più le parentesi graffe e nessuna implementazione di default. Ora dice solo: "Ogni animale deve saper emettere un suono. Come — lo decideranno quelli che erediteranno da me."
Regola importante: Se la tua classe eredita da una classe astratta e non è anch'essa astratta, deve sovrascrivere (con override) tutti i metodi astratti della classe base. Non è opzionale, è un obbligo, un contratto! Il compilatore C# è molto severo su questo. Se ti dimentichi, te lo ricorda subito:
public class Dog : Animal // Classe normale, non astratta
{
// ERRORE DI COMPILAZIONE!
// 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
// (La classe 'Dog' non implementa il membro astratto ereditato 'Animal.MakeSound()')
// Il compilatore si aspetta da noi MakeSound() con override!
}
Per eliminare l'errore, Dog e Cat devono per forza sovrascrivere MakeSound():
public class Dog : Animal
{
public override void MakeSound() // Da sovrascrivere obbligatoriamente!
{
Console.WriteLine("Bau-bau!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Anche qui!
{
Console.WriteLine("Miao!");
}
}
Confronto tra virtual, abstract e override
| Caratteristica | virtual metodo | abstract metodo | override parola chiave |
|---|---|---|---|
| Dove si trova | In una classe normale o astratta | Solo in una classe astratta | Nella classe derivata (figlia) |
| Corpo del metodo | Ha corpo (implementazione di default) | Non ha corpo (finisce con ;) | Ha corpo (nuova implementazione) |
| Scopo | Fornisce un'implementazione di default, ma permette alle derivate di cambiarla | Dichiara un contratto: le derivate devono fornire la loro implementazione | Fornisce un'implementazione specifica per il metodo virtual o abstract della classe base |
| Si può creare un'istanza della classe base? | Sì (se la classe base non è astratta) | No (se la classe base è astratta) | N/A (riguarda il metodo, non la classe) |
| Obbligo per la classe derivata | Può sovrascrivere (override) opzionalmente | Deve sovrascrivere (override), se la derivata non è anch'essa astratta | N/A |
4. Polimorfismo in azione con i metodi astratti
E qui viene il bello! Sappiamo già che il polimorfismo ci permette di lavorare con oggetti di classi derivate diverse tramite un riferimento comune alla classe base. E funziona alla grande anche quando la classe base è astratta!
Anche se non possiamo creare un'istanza di Animal direttamente (ricorda, new Animal() dà errore), possiamo usare il tipo Animal come riferimento a oggetti delle classi derivate. Ed è una cosa potentissima!
Continuiamo con il nostro zoo. Immagina di avere una fattoria con diversi animali. Vogliamo che ogni animale emetta il suo suono.
using System;
// Classe astratta Animal
public abstract class Animal
{
public string Name;
public int Age;
public Animal(string name, int age) { Name = name; Age = age; }
public abstract void MakeSound();
public void Sleep() { Console.WriteLine($"{Name} dorme."); }
}
public class Dog : Animal
{
public Dog(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Bau-bau!"); }
}
public class Cat : Animal
{
public Cat(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Miao!"); }
}
public class Fish : Animal
{
public Fish(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Blub-blub"); }
}
class Program
{
static void Main()
{
Animal[] animals = {
new Dog("Sharik", 3),
new Cat("Murzik", 5),
new Fish("Nemo", 1)
};
foreach (Animal animal in animals)
{
Console.WriteLine($"\nCiao, sono {animal.Name}, ho {animal.Age} anni.");
animal.MakeSound();
animal.Sleep();
}
}
}
Cosa succede in questo codice?
- Abbiamo dichiarato Animal come abstract class. Questo dice al compilatore: "Questa classe è un modello, non puoi crearla direttamente, ma puoi ereditarla".
- Abbiamo dichiarato public abstract void MakeSound(); dentro Animal. Questo dice: "Ogni classe che eredita da Animal (e non è anch'essa astratta), deve implementare il metodo MakeSound()". È il nostro contratto!
- Dog, Cat e Fish rispettano senza fiatare questo contratto, sovrascrivendo MakeSound() con la loro implementazione unica. Se ci fossimo dimenticati di farlo anche solo per una di loro, il compilatore non ci avrebbe fatto passare il codice.
- Nel metodo Main creiamo un array Animal[]. Anche se Animal è astratta, l'array può contenere riferimenti a oggetti delle classi derivate (Dog, Cat, Fish), perché loro sono Animal!
- Quando cicliamo sull'array con foreach e chiamiamo animal.MakeSound(), grazie al polimorfismo C# "sa" quale MakeSound() chiamare: Dog.MakeSound(), Cat.MakeSound() o Fish.MakeSound(). Viene chiamato il metodo relativo al tipo reale dell'oggetto a cui punta Animal in quel momento, non al tipo del riferimento. Questa è la magia del polimorfismo!
- Invece animal.Sleep() chiama l'implementazione concreta dalla classe base Animal, perché quel metodo non è stato marcato come virtual o abstract, e non è stato sovrascritto nelle classi figlie.
5. A cosa serve tutto questo nella vita reale?
"Ok, zoo, animali... Ma dove mi serve quando devo scrivere una vera app per una banca o un negozio?" — ti starai chiedendo. E questa è una domanda top! Le classi e i metodi astratti sono uno strumento potentissimo per progettare sistemi flessibili ed estendibili.
Forzare l'implementazione del contratto: È il vantaggio principale. Immagina di sviluppare un framework per sistemi di pagamento. Hai una classe base abstract class PaymentProcessor (ProcessorePagamenti). E sai per certo che ogni processore di pagamenti deve saper fare ProcessPayment() (ProcessaPagamento), RefundPayment() (RimborsoPagamento) e CheckStatus() (ControllaStato). Ma come funziona per PayPal, carta di credito o Bitcoin — sono cose completamente diverse.
Dichiari questi metodi come abstract in PaymentProcessor.
public abstract class PaymentProcessor
{
public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
public abstract bool RefundPayment(string transactionId);
public abstract string CheckStatus(string transactionId);
// ... altri metodi che possono essere anche concreti, tipo il logging
public void LogTransaction(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
public class PayPalProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Qui logica complessa per lavorare con le API di PayPal
Console.WriteLine($"PayPal: elaboriamo {amount} {currency}...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "Completato"; }
}
public class CreditCardProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Qui logica per lavorare con la banca
Console.WriteLine($"CreditCard: {amount} {currency} dalla carta {cardNumber.Substring(0,4)}XXXX...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "In elaborazione"; }
}
Ora ogni sviluppatore che vorrà creare un nuovo processore di pagamenti (tipo BitcoinProcessor) sarà costretto a implementare tutti e tre questi metodi. Non potrà dimenticarsi per sbaglio di RefundPayment(), perché il compilatore non glielo permetterà! Così la tua architettura resta coerente.
Flessibilità ed estendibilità: Puoi scrivere codice che lavora con PaymentProcessor senza sapere quale implementazione concreta verrà usata. Ad esempio, nel codice del carrello di un e-commerce chiami semplicemente currentProcessor.ProcessPayment(), e il sistema userà il processore giusto in base al metodo di pagamento scelto dall'utente. Domani arriva un nuovo metodo di pagamento — crei una nuova classe che eredita da PaymentProcessor, implementi i metodi astratti, e il codice principale del negozio non devi nemmeno toccarlo!
Evitare implementazioni vuote: Se avessimo usato i metodi virtual invece di abstract, avremmo dovuto dare loro un'implementazione di default vuota, che potrebbe essere misleading (fuorviante). abstract dice chiaramente: "Qui non c'è implementazione, e non ci deve essere — vai dagli eredi!"
Migliorare l'architettura del codice: Le classi astratte aiutano a separare chiaramente la logica comune da quella specifica. Quella comune (tipo LogTransaction in PaymentProcessor) sta nella classe base, quella specifica (come ProcessPayment) — nelle derivate. Così il codice è più leggibile, manutenibile e testabile. Permette ai designer di framework e librerie di definire i "punti di estensione" che gli utenti delle loro librerie devono riempire.
6. Errori comuni e particolarità
Errore n°1: tentare di creare un'istanza di una classe astratta.
È sempre un errore di compilazione. Una classe astratta è un concetto, non un oggetto concreto. Puoi usarla come tipo di riferimento, ma non puoi scrivere new Animal().
Errore n°2: dimenticarsi di sovrascrivere un metodo astratto.
Se la classe derivata non è astratta, deve implementare tutti i metodi abstract della classe base. Altrimenti — errore di compilazione. L'unico modo per evitarlo è rendere anche la derivata astratta, ma di solito non è quello che vuoi.
Errore n°3: confusione tra abstract e virtual.
abstract obbliga a sovrascrivere il metodo, non ha corpo. virtual fornisce un'implementazione di default e può essere sovrascritto. Non puoi dichiarare un metodo abstract con il corpo o un metodo virtual senza corpo — è un errore di sintassi.
Errore n°4: usare new invece di override.
Se non usi override ma crei semplicemente un metodo con lo stesso nome nella derivata, non stai sovrascrivendo, ma nascondendo il metodo della base. Questo porta a comportamenti strani con le chiamate polimorfiche: verrà chiamato il metodo della base.
public class Base
{
public void DoSomething() { Console.WriteLine("Base"); }
}
public class Derived : Base
{
public new void DoSomething() { Console.WriteLine("Derived"); }
}
Base obj = new Derived();
obj.DoSomething(); // Stamperà: Base
GO TO FULL VERSION