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 nel tipo esteso. Per il codice client scritto in C#, F# e Visual Basic, non esiste alcuna differenza apparente tra la chiamata di un metodo di estensione e i metodi definiti in un tipo.
I metodi di estensione più comuni sono gli operatori di query standard LINQ che aggiungono funzionalità di query ai tipi e System.Collections.Generic.IEnumerable<T> esistentiSystem.Collections.IEnumerable. 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 di query standard accettano espressioni lambda come parametri, ma questo non è 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 è preceduto dal modificatore . 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 all'interno di 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();
Richiamare il metodo di estensione nel codice con la sintassi del metodo di istanza. Il linguaggio intermedio (IL) generato dal compilatore converte il codice in una chiamata al metodo statico. Il principio dell'incapsulamento non viene veramente violato. I metodi di estensione non possono accedere alle variabili private nel tipo che stanno estendendo.
Sia la MyExtensions
classe che il WordCount
metodo sono static
e possono essere accessibili come tutti gli altri static
membri. Il WordCount
metodo può essere richiamato come altri static
metodi come indicato di seguito:
string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);
Il codice C# precedente:
- Dichiara e assegna un nuovo
string
oggetto denominatos
con un valore ."Hello Extension Methods"
- Chiama
MyExtensions.WordCount
l'argomento specificatos
Per altre informazioni, vedere Come implementare e chiamare un metodo di estensione personalizzato.
In generale, probabilmente chiamerai metodi di estensione molto più spesso rispetto all'implementazione personalizzata. 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;
Potrebbe anche essere necessario aggiungere un riferimento a System.Core.dll. Si noterà che gli operatori di query standard vengono ora visualizzati in IntelliSense come metodi aggiuntivi disponibili per la maggior parte dei IEnumerable<T> tipi.
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, cercherà eventuali metodi di estensione definiti per il tipo ed eseguirà l'associazione al primo metodo di estensione trovato. Nell'esempio seguente viene dimostrato come il compilatore determina a quale metodo di estensione o metodo di istanza eseguire l'associazione.
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 riesce a trovare un metodo di istanza con una firma corrispondente, verrà associato a un metodo di estensione corrispondente, se presente.
// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
using System;
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, è stato comune creare "Classi di raccolta" che implementa l'interfaccia System.Collections.Generic.IEnumerable<T> per un determinato tipo e funzionalità contenute che agiscono sulle 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 System.Array oggetto 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à di Layer-Specific
Quando si usa un'architettura di Cipolla o un'altra progettazione di applicazioni a più livelli, è comune avere un set di entità di dominio o oggetti di trasferimento dati che possono essere usati per comunicare tra i limiti dell'applicazione. Questi oggetti in genere non contengono funzionalità o solo funzionalità minime che si applicano a tutti i livelli dell'applicazione. I metodi di estensione possono essere usati per aggiungere funzionalità specifiche per 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
Anziché 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 Engine
classe o Query
per eseguire il lavoro di esecuzione di una query in un SQL Server che può essere chiamato da più posizioni nel codice. Tuttavia, è possibile estendere la System.Data.SqlClient.SqlConnection classe usando i metodi di estensione per eseguire tale query da qualsiasi punto in cui è disponibile una connessione a un SQL Server. Altri esempi possono essere l'aggiunta di funzionalità comuni alla System.String classe , l'estensione delle funzionalità di elaborazione dati degli System.IO.File oggetti e System.IO.Stream e System.Exception gli oggetti per funzionalità di gestione degli errori specifiche. Questi tipi di casi d'uso sono limitati solo dalla vostra immaginazione e dal buon senso.
L'estensione dei tipi predefiniti può essere difficile con struct
i tipi 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 dopo la chiusura del metodo di estensione. È possibile aggiungere il ref
modificatore al primo argomento di un metodo di estensione. L'aggiunta del ref
modificatore 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.
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 originale non è sotto il 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 usa un metodo di estensione per estendere un tipo il cui codice sorgente non è in controllo, si corre il rischio che una modifica nell'implementazione del tipo provocherà 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 verrà mai chiamato se dispone della stessa firma di un metodo definito nel tipo.
- I metodi di estensione vengono inseriti nell'ambito al livello dello spazio dei nomi. Ad esempio, se sono presenti più classi statiche che contengono metodi di estensione in un singolo spazio dei nomi denominato
Extensions
, verranno tutti inseriti nell'ambito dallausing Extensions;
direttiva .
Per una libreria di classi implementata, non è necessario utilizzare i metodi di estensione per evitare l'incremento del numero di versione di un assembly. Per aggiungere funzionalità significative a una libreria per cui si è proprietari del codice sorgente, seguire le linee guida .NET per il controllo delle versioni degli assembly. Per altre informazioni, vedere Controllo delle versioni degli assembly.
Vedi anche
- Guida per programmatori C#
- 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