Metodi di estensione (Guida per programmatori C#)
I metodi di estensione consentono di "aggiungere" metodi ai tipi esistenti senza creare un nuovo tipo derivato, ricompilare o modificare in altro modo il tipo originale. I metodi di estensione sono metodi statici, ma vengono chiamati come se fossero metodi di istanza sul tipo esteso. Per il codice client scritto in C#, F# e Visual Basic non esistono differenze evidenti tra la chiamata a un metodo di estensione e ai metodi definiti in un tipo.
I metodi di estensione più comuni sono gli operatori query standard LINQ che aggiungono la funzionalità di query ai tipi System.Collections.IEnumerable e System.Collections.Generic.IEnumerable<T> esistenti. Per utilizzare gli operatori query standard, inserirli innanzitutto nell'ambito con una direttiva using System.Linq
. In questo modo qualsiasi tipo che implementa IEnumerable<T> avrà apparentemente metodi di istanza quali GroupBy, OrderBy, Averagee così via. È possibile visualizzare questi metodi aggiuntivi con la funzionalità di completamento istruzioni di IntelliSense quando si digita "punto" dopo un'istanza di un tipo IEnumerable<T>, ad esempio List<T> o Array.
Esempio di OrderBy
Nell'esempio seguente viene illustrato come chiamare il metodo OrderBy
dell'operatore query standard su una matrice di Integer. L'espressione tra parentesi è un'espressione lambda. Molti operatori query standard accettano espressioni lambda come parametri, sebbene non sia un requisito per i metodi di estensione. Per altre informazioni, vedere Espressioni lambda.
class ExtensionMethods2
{
static void Main()
{
int[] ints = [10, 45, 15, 39, 21, 26];
var result = ints.OrderBy(g => g);
foreach (var i in result)
{
System.Console.Write(i + " ");
}
}
}
//Output: 10 15 21 26 39 45
I metodi di estensione sono definiti come metodi statici, ma vengono chiamati utilizzando la sintassi del metodo di istanza. Il primo parametro specifica il tipo su cui opera il metodo. Il parametro è conforme al modificatore this. I metodi di estensione si trovano nell'ambito solo quando si importa in modo esplicito lo spazio dei nomi nel codice sorgente con una direttiva using
.
Nell'esempio riportato di seguito viene illustrato un metodo di estensione definito per la classe System.String. Viene definito in una classe statica non annidata e non generica:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
Il metodo di estensione WordCount
può essere inserito nell'ambito con questa direttiva using
:
using ExtensionMethods;
Può inoltre essere chiamato da un'applicazione utilizzando questa sintassi:
string s = "Hello Extension Methods";
int i = s.WordCount();
Nel codice si richiama il metodo di estensione con la sintassi del metodo di istanza. Microsoft Intermediate Language (IL) generato dal compilatore converte il codice in una chiamata sul metodo statico. Il principio di incapsulamento non viene realmente violato. I metodi di estensione non possono accedere a variabili private nel tipo che stanno estendendo.
Sia la classe MyExtensions
che il metodo WordCount
sono static
e possono essere accessibili come tutti gli altri membri static
. Il metodo WordCount
può essere richiamato come altri metodi static
come segue:
string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);
Il codice C# precedente:
- Dichiara e assegna un nuovo
string
denominatos
con il valore"Hello Extension Methods"
. - Chiama l'argomento specificato
s
diMyExtensions.WordCount
.
Per altre informazioni, vedere Procedura: Implementare e chiamare un metodo di estensione personalizzato.
In generale, è molto più frequente chiamare i metodi di estensione che implementarne di personalizzati. Perché i metodi di estensione vengono chiamati utilizzando la sintassi del metodo di istanza, non è necessaria alcuna particolare conoscenza per utilizzarli dal codice client. Per abilitare i metodi di estensione per un particolare tipo, aggiungere una direttiva using
per lo spazio dei nomi nel quale sono definiti i metodi. Per utilizzare ad esempio gli operatori query standard, aggiungere questa direttiva using
al codice:
using System.Linq;
Può inoltre essere necessario aggiungere un riferimento a System.Core.dll. Si noterà che gli operatori query standard vengono ora visualizzati in IntelliSense come metodi aggiuntivi disponibili per la maggior parte dei tipi IEnumerable<T>.
Associazione di metodi di estensione in fase di compilazione
È possibile utilizzare metodi di estensione per estendere una classe o un'interfaccia, ma non per eseguirne l'override. Un metodo di estensione con lo stesso nome e la stessa firma di un metodo di interfaccia o di classe non verrà mai chiamato. In fase di compilazione, i metodi di estensione hanno sempre una priorità più bassa dei metodi di istanza definiti nel tipo stesso. In altre parole, se un tipo dispone di un metodo denominato Process(int i)
e si dispone di un metodo di estensione con la stessa firma, il compilatore eseguirà sempre l'associazione al metodo di istanza. Quando il compilatore rileva una chiamata al metodo, cerca innanzitutto una corrispondenza nei metodi di istanza del tipo. Se non viene trovata alcuna corrispondenza, cerca eventuali metodi di estensione definiti per il tipo ed esegue l'associazione al primo metodo di estensione trovato.
Esempio
Nell'esempio seguente vengono illustrate le regole che il compilatore C# segue nel determinare se associare una chiamata al metodo a un metodo di istanza sul tipo o a un metodo di estensione. La classe Extensions
statica contiene metodi di estensione definiti per qualsiasi tipo che implementa IMyInterface
. Le classi A
, B
e C
implementano tutte l'interfaccia.
Il metodo di estensione MethodB
non viene mai chiamato perché il nome e la firma corrispondono esattamente a metodi già implementati dalle classi.
Quando il compilatore non è in grado di trovare un metodo di istanza con una firma corrispondente, eseguirà l'associazione a un metodo di estensione corrispondente se esistente.
// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
public interface IMyInterface
{
// Any class that implements IMyInterface must define a method
// that matches the following signature.
void MethodB();
}
}
// Define extension methods for IMyInterface.
namespace Extensions
{
using System;
using DefineIMyInterface;
// The following extension methods can be accessed by instances of any
// class that implements IMyInterface.
public static class Extension
{
public static void MethodA(this IMyInterface myInterface, int i)
{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, int i)");
}
public static void MethodA(this IMyInterface myInterface, string s)
{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, string s)");
}
// This method is never called in ExtensionMethodsDemo1, because each
// of the three classes A, B, and C implements a method named MethodB
// that has a matching signature.
public static void MethodB(this IMyInterface myInterface)
{
Console.WriteLine
("Extension.MethodB(this IMyInterface myInterface)");
}
}
}
// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
using DefineIMyInterface;
class A : IMyInterface
{
public void MethodB() { Console.WriteLine("A.MethodB()"); }
}
class B : IMyInterface
{
public void MethodB() { Console.WriteLine("B.MethodB()"); }
public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}
class C : IMyInterface
{
public void MethodB() { Console.WriteLine("C.MethodB()"); }
public void MethodA(object obj)
{
Console.WriteLine("C.MethodA(object obj)");
}
}
class ExtMethodDemo
{
static void Main(string[] args)
{
// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();
// For a, b, and c, call the following methods:
// -- MethodA with an int argument
// -- MethodA with a string argument
// -- MethodB with no argument.
// A contains no MethodA, so each call to MethodA resolves to
// the extension method that has a matching signature.
a.MethodA(1); // Extension.MethodA(IMyInterface, int)
a.MethodA("hello"); // Extension.MethodA(IMyInterface, string)
// A has a method that matches the signature of the following call
// to MethodB.
a.MethodB(); // A.MethodB()
// B has methods that match the signatures of the following
// method calls.
b.MethodA(1); // B.MethodA(int)
b.MethodB(); // B.MethodB()
// B has no matching method for the following call, but
// class Extension does.
b.MethodA("hello"); // Extension.MethodA(IMyInterface, string)
// C contains an instance method that matches each of the following
// method calls.
c.MethodA(1); // C.MethodA(object)
c.MethodA("hello"); // C.MethodA(object)
c.MethodB(); // C.MethodB()
}
}
}
/* Output:
Extension.MethodA(this IMyInterface myInterface, int i)
Extension.MethodA(this IMyInterface myInterface, string s)
A.MethodB()
B.MethodA(int i)
B.MethodB()
Extension.MethodA(this IMyInterface myInterface, string s)
C.MethodA(object obj)
C.MethodA(object obj)
C.MethodB()
*/
Modelli di utilizzo comuni
Funzionalità di raccolta
In passato, era comune creare "Classi di raccolta" che implementassero l'interfaccia System.Collections.Generic.IEnumerable<T> per un determinato tipo e che contenessero funzionalità che agivano su raccolte di quel tipo. Anche se non c'è nulla di sbagliato con la creazione di questo tipo di oggetto raccolta, è possibile ottenere la stessa funzionalità usando un'estensione in System.Collections.Generic.IEnumerable<T>. Le estensioni hanno il vantaggio di consentire la chiamata della funzionalità da qualsiasi raccolta, ad esempio un oggetto System.Array o System.Collections.Generic.List<T> che implementa System.Collections.Generic.IEnumerable<T> su tale tipo. Un esempio di questo uso di una matrice di Int32 è disponibile in precedenza in questo articolo.
Funzionalità specifica del livello
Quando si usa un'architettura Onion o un'altra progettazione di applicazioni a più livelli, è comune avere un set di entità di dominio o oggetti di trasferimento dati che possano essere usati per comunicare attraverso i limiti dell'applicazione. Questi oggetti in genere non contengono funzionalità o ne hanno solo alcune minime che si applicano a tutti i livelli dell'applicazione. I metodi di estensione possono essere usati per aggiungere funzionalità specifiche di ogni livello applicazione senza caricare l'oggetto con metodi non necessari o desiderati in altri livelli.
public class DomainEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
static class DomainEntityExtensions
{
static string FullName(this DomainEntity value)
=> $"{value.FirstName} {value.LastName}";
}
Estensione dei tipi predefiniti
Invece di creare nuovi oggetti quando è necessario creare funzionalità riutilizzabili, è spesso possibile estendere un tipo esistente, ad esempio un tipo .NET o CLR. Ad esempio, se non si usano metodi di estensione, è possibile creare una classe Engine
o Query
per eseguire il lavoro di esecuzione di una query in un'istanza di SQL Server che può essere chiamata da più posizioni nel codice. Tuttavia, è possibile estendere la classe System.Data.SqlClient.SqlConnection usando metodi di estensione per eseguire tale query da qualsiasi punto in cui si dispone di una connessione a SQL Server. Altri esempi possono essere l'aggiunta di funzionalità comuni alla classe System.String, l'estensione delle funzionalità di elaborazione dati dell'oggetto System.IO.Stream e gli oggetti System.Exception per funzionalità di gestione degli errori specifiche. Questi tipi di casi d'uso sono limitati solo dall'immaginazione e dal buon senso.
L'estensione dei tipi predefiniti può essere difficile con i tipi struct
perché vengono passati per valore ai metodi. Ciò significa che tutte le modifiche apportate allo struct vengono apportate a una copia dello struct. Queste modifiche non sono visibili al termine del metodo di estensione. È possibile aggiungere il modificatore ref
al primo argomento rendendolo un metodo di estensione ref
. La parola chiave ref
può essere visualizzata prima o dopo la parola chiave this
senza alcuna differenza semantica. L'aggiunta del modificatore ref
indica che il primo argomento viene passato per riferimento. In questo modo è possibile scrivere metodi di estensione che modificano lo stato dello struct da estendere (si noti che i membri privati non sono accessibili). Solo i tipi valore o i tipi generici vincolati allo struct (vedere vincolo struct
per altre informazioni) sono consentiti come primo parametro di un metodo di estensione ref
. L'esempio seguente illustra come usare un metodo di estensione ref
per modificare direttamente un tipo predefinito senza dover riassegnare il risultato o passarlo attraverso una funzione con la parola chiave ref
:
public static class IntExtensions
{
public static void Increment(this int number)
=> number++;
// Take note of the extra ref keyword here
public static void RefIncrement(this ref int number)
=> number++;
}
public static class IntProgram
{
public static void Test()
{
int x = 1;
// Takes x by value leading to the extension method
// Increment modifying its own copy, leaving x unchanged
x.Increment();
Console.WriteLine($"x is now {x}"); // x is now 1
// Takes x by reference leading to the extension method
// RefIncrement changing the value of x directly
x.RefIncrement();
Console.WriteLine($"x is now {x}"); // x is now 2
}
}
L'esempio seguente illustra i metodi di estensione ref
per i tipi di struct definiti dall'utente:
public struct Account
{
public uint id;
public float balance;
private int secret;
}
public static class AccountExtensions
{
// ref keyword can also appear before the this keyword
public static void Deposit(ref this Account account, float amount)
{
account.balance += amount;
// The following line results in an error as an extension
// method is not allowed to access private members
// account.secret = 1; // CS0122
}
}
public static class AccountProgram
{
public static void Test()
{
Account account = new()
{
id = 1,
balance = 100f
};
Console.WriteLine($"I have ${account.balance}"); // I have $100
account.Deposit(50f);
Console.WriteLine($"I have ${account.balance}"); // I have $150
}
}
Linee guida generali
Anche se è ancora considerato preferibile aggiungere funzionalità modificando il codice di un oggetto o derivando un nuovo tipo ogni volta che è ragionevole e possibile farlo, i metodi di estensione sono diventati un'opzione fondamentale per la creazione di funzionalità riutilizzabili in tutto l'ecosistema .NET. Per quelle occasioni in cui l'origine di partenza non è sotto il proprio controllo, quando un oggetto derivato è inappropriato o impossibile o quando la funzionalità non deve essere esposta oltre l'ambito applicabile, i metodi di estensione sono una scelta eccellente.
Per altre informazioni sui tipi derivati, vedere Ereditarietà.
Quando si utilizza un metodo di estensione per estendere un tipo di cui non è possibile modificare il codice sorgente, si corre il rischio che una modifica nell'implementazione del tipo provochi l'interruzione del metodo di estensione.
Se si implementano metodi di estensione per un determinato tipo, è importante tenere presente quanto segue:
- Un metodo di estensione non viene chiamato se ha la stessa firma di un metodo definito nel tipo.
- I metodi di estensione vengono inseriti nell'ambito al livello dello spazio dei nomi. Se, ad esempio, si dispone di più classi statiche contenenti metodi di estensione in un solo spazio dei nomi denominato
Extensions
, verranno tutti inseriti nell'ambito dalla direttivausing Extensions;
.
Per una libreria di classi implementata, non è necessario utilizzare i metodi di estensione per evitare l'incremento del numero di versione di un assembly. Se si desidera aggiungere funzionalità significative a una libreria per il quale si è proprietari del codice sorgente, seguire le linee guida di .NET per il controllo delle versioni degli assembly. Per altre informazioni, vedere Controllo delle versioni degli assembly.
Vedi anche
- Esempi di programmazione parallela (sono inclusi molti metodi di estensione di esempio)
- Espressioni lambda
- Cenni preliminari sugli operatori di query standard
- Regole di conversione per parametri Instance e relativo impatto
- Interoperabilità dei metodi di estensione tra linguaggi
- Metodi di estensione e delegati sottoposti a currying
- Associazione di metodi di estensione e segnalazione errori