Un metodo di estensione aggiunge un metodo a un tipo esistente senza modificarlo, senza ereditare da esso e senza incapsularlo. Scrivi un normale metodo statico, marchi il primo parametro con this, e chi lo usa lo chiama con la sintassi del punto, come se il tipo lo avesse sempre avuto:
Output:
Extension methods let yo...
12
short
Extension...
title.Truncate(24) viene compilato esattamente come StringExtensions.Truncate(title, 24). La sintassi del punto è solo una comodità: il metodo resta statico, vive sempre in StringExtensions e può usare solo i membri pubblici di string.
Le regole per dichiararne uno
- Il metodo deve essere
statice stare in una classestatic, non generica e non annidata in un'altra classe. Una classe non statica o generica dà CS1106,Extension method must be defined in a non-generic static class; una classe annidata dà CS1109,Extension methods must be defined in a top level static class. thisva solo sul primo parametro, e il tipo di quel parametro è il tipo che viene esteso. Gli altri parametri sono normali.- Il metodo può essere generico a sua volta (
this IEnumerable<T> source), anche se la classe non può esserlo. - Vede solo quello che vede qualsiasi codice esterno: i membri pubblici (e, nello stesso assembly, quelli internal). I campi privati restano privati.
La convenzione è dare alla classe il nome di ciò che estende (StringExtensions, EnumerableExtensions) e raggruppare queste classi in un namespace come MyApp.Extensions.
Estendere interfacce e IEnumerable<T>
Estendere un'interfaccia aggiunge il metodo a ogni tipo che la implementa. Estendere IEnumerable<T> dà quindi il metodo in un colpo solo ad array, liste, set, collezioni di chiavi dei dizionari e risultati delle query LINQ:
Output:
9.5
50
65
f0 f3 f6
EveryNth è generico: T viene dedotto dal receiver, quindi qui funziona con le stringhe e con qualsiasi altro tipo di elemento. Dato che usa yield return, è lazy come gli operatori LINQ predefiniti e si può concatenare con essi.
LINQ è costruito sui metodi di estensione
Where, Select, OrderBy, Sum, First e il resto di LINQ sono metodi di estensione su IEnumerable<T>, definiti nella classe statica System.Linq.Enumerable. Ecco perché using System.Linq; deve stare in cima al file prima che list.Where(...) compili, e perché LINQ funziona su ogni tipo di collezione senza che nessuna implementi un metodo Where:
Output:
72, 88, 95
72, 88, 95
Le due righe sono lo stesso codice. La sintassi di estensione trasforma chiamate annidate, che si leggono dall'interno verso l'esterno, in una catena che si legge nell'ordine in cui avvengono i passaggi. È il motivo principale per cui esistono i metodi di estensione: sono stati aggiunti in C# 3 insieme a LINQ. La pagina su LINQ tratta gli operatori.
Il namespace deve essere importato
Un metodo di estensione è visibile solo quando il namespace della sua classe statica è importato con using. Senza, la chiamata fallisce come se il metodo non esistesse:
error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
Aggiungi il using per il namespace delle estensioni (oppure, da C# 10, un global using una sola volta per tutto il progetto):
Output:
EUR 49.90
Il rovescio della medaglia è utile: un metodo di estensione che non vuoi ovunque può stare in un namespace che importano solo alcuni file.
Vincono i metodi di istanza
Quando un tipo ha già un metodo di istanza applicabile, il compilatore usa quello e non considera mai l'estensione. Un'estensione con la stessa firma è codice morto in ogni chiamata che usa la sintassi del punto:
Output:
Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call
L'estensione con un argomento ha perso contro il metodo di istanza; quella con due argomenti è stata usata perché nessun metodo di istanza accetta una stringa. Questo significa anche che un aggiornamento di una libreria che aggiunge un metodo di istanza con il nome della tua estensione sposta in silenzio le tue chiamate sulla versione della libreria. Scegli nomi di estensione che difficilmente entrino in conflitto.
Chiamate su null
Dato che la chiamata è in realtà una chiamata statica, un receiver null non lancia eccezioni nel punto della chiamata. Il metodo riceve null come primo argomento e decide cosa significa:
Output:
True
AL
ArgumentNullException: fullName
Che missing.IsBlank() restituisca true è comodo, ma può sorprendere chi si aspetta che una chiamata a un membro su null lanci un'eccezione. Accetta null solo nei metodi il cui nome lo rende ovvio (IsBlank, OrEmpty); altrove lancia ArgumentNullException come fa LINQ.
Estendere gli enum
Gli enum non possono avere metodi propri, e questo li rende un bersaglio naturale:
enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }
static class OrderStatusExtensions
{
public static bool IsFinal(this OrderStatus s) =>
s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}
// usage: if (order.Status.IsFinal()) { ... }
Membri di estensione in C# 14
Fino a C# 14 solo i metodi potevano essere estensioni. C# 14 ha aggiunto i blocchi extension, che raggruppano i membri per un receiver e permettono anche proprietà di estensione e membri statici:
// C# 14 and later
public static class StringExtensions
{
extension(string s)
{
public bool IsBlank => string.IsNullOrWhiteSpace(s); // extension property
public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
}
}
I classici metodi di estensione con il parametro this restano validi e sono quelli che usano quasi tutto il codice e le librerie esistenti.
Quando scriverne uno
I metodi di estensione sono adatti quando il tipo non è tuo (string, DateTime, interfacce del framework), quando vuoi lo stesso helper su ogni implementazione di un'interfaccia, o quando una catena si legge meglio di chiamate annidate. Quando la classe è tua, aggiungi invece un vero metodo di istanza: può usare lo stato privato e compare dove chi legge lo va a cercare. Ed evita di estendere object, che mette il tuo metodo nell'elenco dei completamenti di ogni valore del programma.
Domande frequenti
Che cos'è un metodo di estensione in C#?
Un metodo statico che si può chiamare come se fosse un metodo di istanza di un altro tipo. Lo scrivi in una classe statica e metti this davanti al primo parametro: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. A quel punto name.IsBlank() funziona su qualsiasi stringa, senza modificare string né ereditare da essa.
Perché il mio metodo di estensione non viene trovato?
I metodi di estensione sono visibili solo quando il loro namespace è importato. Se la classe statica sta in MyApp.Extensions, aggiungi using MyApp.Extensions; al file che la chiama; altrimenti il compilatore segnala CS1061, dicendo che il tipo non contiene una definizione né un metodo di estensione accessibile con quel nome. Controlla anche che la classe sia static, non generica e non annidata.
Un metodo di estensione può sostituire un metodo di istanza?
No. Il compilatore cerca prima un metodo di istanza applicabile e considera i metodi di estensione solo quando non ne esiste nessuno. Un'estensione con lo stesso nome e gli stessi parametri di un metodo di istanza viene ignorata in silenzio in ogni chiamata che usa la sintassi di istanza.
Si può chiamare un metodo di estensione su null in C#?
Sì. La chiamata viene compilata come chiamata a un metodo statico con null come primo argomento, quindi la chiamata in sé non lancia NullReferenceException. È il metodo a decidere cosa fare: trattare null come input valido (come un helper in stile IsNullOrEmpty) oppure lanciare ArgumentNullException, come fanno i metodi di LINQ.
Esistono le proprietà di estensione in C#?
Non prima di C# 14. Fino ad allora solo i metodi possono essere estensioni, quindi una potenziale proprietà si scrive come metodo (GetFullName()). C# 14 ha aggiunto i blocchi extension, che possono dichiarare proprietà di estensione e membri statici oltre ai metodi.