Un'interfaccia è un contratto: un elenco con nome di membri che un tipo promette di fornire. Dice cosa sa fare un tipo e nulla su come lo fa. Classi e struct implementano le interfacce, e il codice scritto per l'interfaccia funziona con ogni implementazione, comprese quelle scritte in seguito.
Dichiarare e implementare un'interfaccia
Un'interfaccia dichiara membri senza corpo. Per convenzione il suo nome inizia con I. Una classe la implementa indicandola dopo i due punti e fornendo un membro pubblico per ciascun membro:
Output:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice non sa nulla di PercentOff o FixedOff. Dipende solo da IDiscount, quindi aggiungere più avanti una classe BuyOneGetOne non richiede di modificarlo. Questo disaccoppiamento è l'intera ragion d'essere delle interfacce.
Alcune regole sulla dichiarazione:
- I membri di un'interfaccia sono pubblici di default, e anche i membri che li implementano devono essere pubblici (a meno che non siano implementati in modo esplicito, vedi sotto).
- Un'interfaccia può dichiarare metodi, proprietà, indicizzatori ed eventi. Non può dichiarare campi di istanza né costruttori.
- Una classe che omette un membro non compila: errore CS0535, "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
- Non puoi creare un'interfaccia con
new. Crei una classe che la implementa e puoi tenerla in una variabile del tipo dell'interfaccia:IDiscount d = new FixedOff(5);.
L'interfaccia come tipo
Una variabile, un parametro, un campo o un tipo di ritorno possono essere dichiarati come interfaccia. Accettano allora qualsiasi oggetto che la implementa, ed espongono solo i membri dell'interfaccia. È così che si scrive codice a cui non importa quale implementazione riceve, ed è così che i test sostituiscono un'implementazione finta:
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
I container di dependency injection di ASP.NET Core si basano su questo: i servizi vengono registrati e richiesti per interfaccia.
Implementare più interfacce
Una classe ha al massimo una classe base ma può implementare un numero qualsiasi di interfacce. Prima la classe base, poi le interfacce, separate da virgole:
Output:
INVOICE Studio rent March: 950.00
True
True
Le interfacce possono anche ereditare da altre interfacce: interface IRepository<T> : IReadRepository<T> aggiunge membri a quelli che eredita, e una classe che implementa IRepository<T> deve fornire entrambi gli insiemi.
Implementazione esplicita di un'interfaccia
A volte due interfacce dichiarano un membro con lo stesso nome e significati diversi, oppure un membro dell'interfaccia non ha senso sulla superficie pubblica della classe. Implementalo in modo esplicito mettendo davanti il nome dell'interfaccia e omettendo il modificatore di accesso:
Output:
Desk lamp,34.90
Desk lamp for 34.90
Un membro implementato in modo esplicito è raggiungibile solo tramite l'interfaccia. Il framework lo usa per tenere pulite le API dei tipi comuni: gli array implementano IList.Add in modo esplicito (lancia NotSupportedException, dato che gli array hanno dimensione fissa), quindi il metodo non compare quando scrivi array..
Le interfacce del framework che implementerai
Implementare un'interfaccia standard collega il tuo tipo al codice esistente del framework. Tre compaiono di continuo.
IComparable<T> dà a un tipo un ordinamento naturale, che usano List<T>.Sort(), Array.Sort e Max(). CompareTo restituisce un numero negativo, zero o un numero positivo:
Output:
1.4, 2.9, 2.10, 10.0
Ordinare le stesse versioni come stringhe darebbe 1.4, 10.0, 2.10, 2.9. Senza IComparable<T>, Sort() lancia InvalidOperationException perché non ha modo di confrontare due oggetti Version.
IEnumerable<T> rende un tipo utilizzabile in foreach e con LINQ. Il modo facile per implementare GetEnumerator è con yield return, trattato nella pagina su IEnumerable e yield.
IDisposable contrassegna un tipo che tiene qualcosa da rilasciare (un handle di file, una connessione, un timer). Il suo unico metodo, Dispose, è quello che l'istruzione using chiama alla fine del blocco, anche se viene lanciata un'eccezione:
Output:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
Metodi predefiniti delle interfacce (C# 8)
Aggiungere un membro a un'interfaccia pubblicata rompeva tutte le classi che la implementavano. Da C# 8, un membro di un'interfaccia può avere un corpo, che le classi che la implementano ereditano a meno che non ne forniscano uno proprio:
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
L'ultima riga è quella che sorprende: un metodo predefinito non viene ereditato tra i membri propri della classe, quindi si può chiamare solo tramite il tipo dell'interfaccia. I metodi predefiniti inoltre non possono usare campi di istanza (le interfacce non ne hanno); lavorano tramite gli altri membri dell'interfaccia. Richiedono .NET Core 3.0 o successivi e non sono disponibili su .NET Framework.
C# 8 ha anche permesso i membri statici nelle interfacce, e C# 11 ha aggiunto i membri static abstract, che permettono al codice generico di chiamare un metodo statico o un operatore su T. È su questo che si basa la matematica generica di .NET 7 (INumber<T>, where T : INumber<T>).
Interfaccia o classe astratta?
Usa un'interfaccia per una capacità che tipi non collegati tra loro possono condividere, quando devono partecipare anche le struct, o quando una classe ha bisogno di più ruoli di questo tipo. Usa una classe astratta quando le implementazioni sono varianti di una stessa cosa e condividono stato o un algoritmo fisso. La pagina sulle classi astratte ha una tabella di confronto. Molti design usano entrambe: un'interfaccia per chi usa il tipo, e una classe base astratta che implementa le parti noiose per chi lo implementa.
Errori comuni
- Implementazione mancante o non pubblica. Ogni membro dell'interfaccia richiede un membro pubblico con firma corrispondente, oppure un'implementazione esplicita. Altrimenti CS0535 (o CS0737 quando il metodo esiste ma non è pubblico).
- Interfacce con una sola implementazione "non si sa mai". Un'interfaccia si guadagna il posto quando ci sono più implementazioni o un finto oggetto per i test. Altrimenti è solo un file in più da navigare.
- Interfacce troppo grandi. Un'interfaccia con venti membri obbliga ogni implementazione a scriverne venti. Dividila in ruoli più piccoli (
IReader,IWriter) che le classi combinano. - Fare il cast di un'interfaccia verso una classe.
((StripeGateway)gateway).Refund()annulla il disaccoppiamento. Se chi chiama ha bisogno diRefund, deve stare nell'interfaccia. - Aspettarsi di chiamare un metodo predefinito dell'interfaccia sulla classe. Si raggiunge tramite il tipo dell'interfaccia.
Domande frequenti
Che cos'è un'interfaccia in C#?
Un'interfaccia è un insieme con nome di membri (metodi, proprietà, eventi, indicizzatori) che un tipo promette di fornire, senza stato di istanza. interface IDiscount { decimal Apply(decimal price); } dice "qualsiasi cosa sia un IDiscount sa applicarsi a un prezzo". Classi e struct la implementano con i due punti, e il codice può poi lavorare con ciascuna di esse tramite il tipo dell'interfaccia.
Come implemento un'interfaccia in C#?
Indicala dopo i due punti nella dichiarazione della classe (dopo la classe base, se c'è) e fornisci un membro pubblico per ogni membro dell'interfaccia: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Un membro mancante dà l'errore CS0535. Visual Studio e Rider possono generare gli stub con una correzione rapida.
Una classe C# può implementare più interfacce?
Sì, quante ne servono, separate da virgole: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. È la risposta di C# all'ereditarietà multipla: una classe ha una sola classe base ma può ricoprire molti ruoli. Se due interfacce dichiarano lo stesso membro, un solo metodo pubblico può soddisfarle entrambe, oppure puoi usare l'implementazione esplicita per dare a ciascuna il suo.
Cos'è l'implementazione esplicita di un'interfaccia in C#?
Implementare un membro con il nome dell'interfaccia davanti e senza modificatore di accesso: string IExportable.Format() { ... }. A quel punto il membro si può chiamare solo tramite una variabile del tipo dell'interfaccia, non tramite la classe. Si usa per risolvere conflitti di nomi tra due interfacce e per tenere fuori dalla superficie pubblica della classe i membri d'interfaccia usati di rado.
Cosa sono i metodi predefiniti delle interfacce in C#?
Da C# 8, un membro di un'interfaccia può avere un corpo: void Error(string m) => Write("ERROR: " + m);. Le classi che la implementano lo ricevono gratis e possono fornirne uno proprio. La funzionalità esiste perché gli autori di librerie possano aggiungere membri a un'interfaccia pubblicata senza rompere tutte le implementazioni. Un metodo predefinito si raggiunge tramite il tipo dell'interfaccia, non tramite la classe.
Perché i nomi delle interfacce C# iniziano con I?
È la convenzione di denominazione di .NET (IEnumerable, IDisposable, IComparable<T>): una I maiuscola seguita da un nome in PascalCase, spesso un aggettivo che termina in "-able". Il compilatore non lo richiede, ma seguirla rende i tipi interfaccia riconoscibili a colpo d'occhio, soprattutto in una dichiarazione di classe dove classe base e interfacce condividono un unico elenco.