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 evidente 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 segue questo 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 nidificata 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 effettivamente 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 statice possono essere accessibili come tutti gli altri static membri. Il WordCount metodo può essere richiamato come altri static metodi come segue:

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

Il codice C# precedente:

  • Dichiara e assegna un nuovo string nome con s il valore "Hello Extension Methods".
  • Chiama MyExtensions.WordCount l'argomento sspecificato.

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 ora vengono 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, cerca i metodi di estensione definiti per il tipo e associa 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 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
{
    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 implementa l'interfaccia System.Collections.Generic.IEnumerable<T> per un determinato tipo e funzionalità contenute che agiscono 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 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à specifiche del livello

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 attraverso 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 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 Engine classe 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 System.Data.SqlClient.SqlConnection classe 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 System.String classe , l'estensione delle funzionalità di elaborazione dati dell'oggetto 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 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 al termine del metodo di estensione. È possibile aggiungere il ref modificatore al primo argomento rendendolo un ref metodo di estensione. La ref parola chiave può essere visualizzata prima o dopo la this parola chiave senza alcuna differenza semantica. 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 (si noti che i membri privati non sono accessibili). Solo i tipi valore o i tipi generici vincolati allo struct (vedere struct vincolo per altre informazioni) sono consentiti come primo parametro di un ref metodo di estensione. L'esempio seguente illustra come usare un ref metodo di estensione per modificare direttamente un tipo predefinito senza dover riassegnare il risultato o passarlo attraverso una funzione con la ref parola chiave :

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
    }
}

Questo esempio seguente illustra i metodi di estensione per i ref 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 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 è controllato, 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. 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 dalla using 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