Una classe astratta è una classe che esiste solo per essere derivata. Non si può istanziare e può dichiarare membri astratti: metodi e proprietà senza corpo che ogni classe derivata concreta deve implementare. Per tutto il resto è una classe normale: può avere campi, costruttori e metodi completamente implementati che le classi derivate ereditano.
Dichiarare una classe astratta
Marca la classe come abstract, e marca come abstract ogni membro che le classi derivate devono fornire, chiudendolo con un punto e virgola al posto del corpo:
Output:
Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00
Describe è scritto una volta sola, nella classe base, e chiama Area e Perimeter, che in Shape non esistono affatto. È proprio questo lo scopo di un membro astratto: la classe base può farci affidamento e il compilatore garantisce che ogni forma concreta lo fornisca. Una classe derivata che se ne dimentica uno non compila:
class Triangle : Shape
{
public Triangle() : base("Triangle") { }
public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'
La soluzione è implementare Perimeter, oppure marcare Triangle stessa come abstract se è una base intermedia per altre classi.
Regole per i membri astratti
- Un membro astratto non ha corpo ed è implicitamente virtuale: le classi derivate lo implementano con
override. - Solo le classi astratte possono contenere membri astratti. Metterne uno in una classe normale è un errore di compilazione.
- I membri astratti non possono essere
private(nessuno potrebbe sovrascriverli) néstaticin una classe. - Metodi, proprietà, indicizzatori ed eventi possono essere astratti. Campi e costruttori no.
- Una classe astratta non deve per forza contenere membri astratti. Marcare una classe
abstractimpedisce semplicemente di istanziarla, cosa utile per una classe base che ha senso solo attraverso le sue sottoclassi.
Una proprietà astratta dichiara quali accessor deve fornire la classe derivata:
abstract class Plan
{
public abstract decimal MonthlyPrice { get; }
public abstract string Name { get; }
}
class ProPlan : Plan
{
public override decimal MonthlyPrice => 12.99m;
public override string Name => "Pro";
}
Costruttori nelle classi astratte
Una classe astratta può avere costruttori. Non si possono chiamare con new, ma ogni costruttore derivato ne esegue uno tramite : base(...), quindi sono il posto in cui lo stato condiviso viene inizializzato e validato. Dichiarali protected, che dice esattamente questo: solo le classi derivate possono usarli.
Il pattern template method
Il motivo più comune per scrivere una classe astratta è fissare l'ordine di un algoritmo nella classe base e lasciare che le classi derivate completino i singoli passi. Il metodo base che esegue i passi non è virtuale, quindi nessuna sottoclasse può riordinarli o saltarli.
Output:
item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20
| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |
Total: 26.20
Ogni riga è una tupla con nomi, e l'output viene costruito con uno StringBuilder. Nota il mix: Header e Row sono abstract perché ogni formato deve deciderli, mentre Footer è virtual con un valore predefinito vuoto perché alla maggior parte dei formati lì non serve nulla. I passi sono protected, quindi il codice esterno può chiamare solo Export. Le classi del framework usano la stessa idea: Stream e TextWriter sono classi base astratte i cui tanti metodi di comodo si basano su pochi membri forniti dalla classe derivata (Read e Write per uno stream, Write(char) per un text writer).
Classe astratta vs interfaccia
Entrambe ti permettono di scrivere codice su un tipo senza conoscere la classe concreta. Si differenziano per cosa possono contenere e per quante ne può avere una classe:
| Classe astratta | Interfaccia | |
|---|---|---|
| Una classe può ereditarne o implementarne | una | molte |
| Campi di istanza (stato) | sì | no |
| Costruttori | sì | no |
| Metodi con corpo | sì | sì, da C# 8 (metodi di interfaccia predefiniti) |
| Membri statici | sì | sì, da C# 8; membri static abstract da C# 11 |
| Modificatori di accesso sui membri | qualsiasi | public per impostazione predefinita; gli altri ammessi da C# 8 |
| Utilizzabile dalle struct | no | sì |
| Aggiungere un membro in seguito | sicuro se non è astratto | rompe chi la implementa, a meno che abbia un corpo predefinito |
I metodi di interfaccia predefiniti (C# 8) hanno ridotto la distanza, ma non la differenza di fondo:
public interface ILogger
{
void Write(string message);
void Error(string message) => Write("ERROR: " + message); // default implementation: C# 8, .NET Core 3.0+
}
Un metodo predefinito non può comunque accedere a campi di istanza, perché l'interfaccia non ne ha; può solo chiamare altri membri dell'interfaccia.
Un modo pratico per scegliere:
- Interfaccia quando descrivi una capacità che possono avere tipi non correlati (
IComparable<T>,IDisposable,IShippingProvider), quando anche le struct devono poter partecipare, o quando una classe ha bisogno di più ruoli di questo tipo. Per questo motivo la maggior parte del codice con dependency injection è scritta su interfacce. - Classe astratta quando i tipi derivati sono davvero varianti di una stessa cosa e condividono uno stato o un algoritmo fisso, come gli exporter qui sopra.
- Entrambe è una scelta comune: un'interfaccia da cui dipendono i chiamanti e una classe base astratta che la implementa e risparmia a chi implementa di riscrivere le parti condivise. La pagina sulle interfacce approfondisce il lato delle interfacce.
Errori comuni
- Provare a istanziare la classe base.
new Shape(...)dà CS0144. Istanzia una classe derivata. - Dimenticare
overridenell'implementazione. Scriverepublic double Area()nella classe derivata non implementa il membro astratto; il compilatore segnala CS0534 per l'implementazione mancante, più un warning che il nuovo metodo nasconde quello ereditato. - Un costruttore pubblico in una classe astratta. Innocuo ma fuorviante. Usa
protected. - Classi astratte senza codice condiviso. Se la classe base ha solo membri astratti e nessuno stato, un'interfaccia dice la stessa cosa e non occupa l'unico posto disponibile per la classe base.
- Chiamare membri astratti dal costruttore base. L'implementazione derivata viene eseguita prima del corpo del costruttore derivato, quindi i campi che quel corpo assegna valgono ancora
0onull.
Domande frequenti
Cos'è una classe astratta in C#?
Una classe marcata abstract serve solo come classe base: non puoi crearne un'istanza con new (errore CS0144). Può contenere campi, costruttori e metodi normali, più membri abstract che non hanno corpo e che ogni classe derivata non astratta deve implementare.
Che differenza c'è tra una classe astratta e un'interfaccia in C#?
Una classe può derivare da una sola classe astratta, ma può implementare molte interfacce. Una classe astratta può avere uno stato (campi di istanza), costruttori e membri con qualsiasi livello di accesso, ed è il posto naturale per l'implementazione condivisa. Un'interfaccia descrive una capacità senza stato di istanza; da C# 8 può includere corpi di metodo predefiniti, ma continua a non poter avere campi di istanza né costruttori.
Che differenza c'è tra metodi abstract e virtual in C#?
Un metodo virtual ha un corpo che le classi derivate possono sovrascrivere. Un metodo abstract non ha corpo e le classi derivate devono sovrascriverlo, altrimenti sono a loro volta astratte (errore CS0534 per una classe concreta). I metodi astratti possono comparire solo in classi astratte.
Una classe astratta può avere un costruttore in C#?
Sì. Viene eseguito quando si costruisce una classe derivata, tramite : base(...), e di solito inizializza i campi condivisi. Dichiaralo protected: un costruttore pubblico in una classe astratta fa pensare che chi la usa possa chiamarlo, e invece non può.
Perché non posso creare un'istanza di una classe astratta?
Perché può avere membri astratti senza implementazione: chiamare new Shape().Area() non avrebbe alcun codice da eseguire. Il compilatore segnala CS0144 ("Cannot create an instance of the abstract type or interface"). Crea invece un'istanza di una classe derivata concreta e, se vuoi, tienila in una variabile del tipo astratto: Shape s = new Circle(2);.