Metody rozszerzeń (Przewodnik programowania w języku C#)

Metody rozszerzenia umożliwiają „dodawanie” metod do istniejących typów bez konieczności tworzenia nowego typu pochodnego, ponownej kompilacji lub modyfikowania oryginalnego typu w inny sposób. Metody rozszerzenia to metody statyczne, ale są wywoływane tak, jakby były metodami wystąpień w typie rozszerzonym. W przypadku kodu klienta napisanego w języku C#, F# i Visual Basic nie ma wyraźnej różnicy między wywoływaniem metody rozszerzenia a metodami zdefiniowanymi w typie.

Najbardziej typowe metody rozszerzenia to standardowe operatory zapytań LINQ, które dodają funkcje zapytania do istniejących System.Collections.IEnumerable typów i System.Collections.Generic.IEnumerable<T> . Aby użyć standardowych operatorów zapytań, najpierw wprowadź je do zakresu z dyrektywą using System.Linq . Następnie dowolny typ, który implementuje IEnumerable<T> , wydaje się mieć metody wystąpienia, takie jak GroupBy, OrderBy, Averagei tak dalej. Te dodatkowe metody można zobaczyć w uzupełnianiu instrukcji IntelliSense po wpisaniu "kropka" po wystąpieniu IEnumerable<T> typu, takiego jak List<T> lub Array.

Przykład OrderBy

W poniższym przykładzie pokazano, jak wywołać standardową metodę operatora OrderBy zapytania na tablicy liczb całkowitych. Wyrażenie w nawiasach to wyrażenie lambda. Wiele standardowych operatorów zapytań bierze wyrażenia lambda jako parametry, ale nie jest to wymagane dla metod rozszerzenia. Aby uzyskać więcej informacji, zobacz Wyrażenia 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

Metody rozszerzenia są zdefiniowane jako metody statyczne, ale są wywoływane przy użyciu składni metod wystąpienia. Ich pierwszy parametr określa typ, na którym działa metoda. Parametr jest zgodny z tym modyfikatorem. Metody rozszerzeń są w zakresie tylko wtedy, gdy jawnie zaimportujesz przestrzeń nazw do kodu źródłowego z dyrektywą using .

W poniższym przykładzie przedstawiono metodę rozszerzenia zdefiniowaną System.String dla klasy. Jest ona zdefiniowana wewnątrz niegnieżdżonej, nieogółowej klasy statycznej:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

WordCount Metodę rozszerzenia można wprowadzić do zakresu za pomocą tej using dyrektywy:

using ExtensionMethods;

A za pomocą poniższej składni można ją wywołać z aplikacji:

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

Wywołasz metodę rozszerzenia w kodzie przy użyciu składni metody wystąpienia. Język pośredni (IL) wygenerowany przez kompilator tłumaczy kod na wywołanie metody statycznej. Zasada hermetyzacji nie jest naprawdę naruszana. Metody rozszerzeń nie mogą uzyskać dostępu do zmiennych prywatnych w typie, który rozszerzają.

MyExtensions Zarówno klasa, jak i WordCount metoda to static, i można uzyskać do niej dostęp, podobnie jak wszystkie inne static elementy członkowskie. Metodę WordCount można wywołać jak inne static metody w następujący sposób:

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

Poprzedni kod języka C#:

  • Deklaruje i przypisuje nową string nazwę o nazwie s z wartością "Hello Extension Methods".
  • Wywołania MyExtensions.WordCount podanego argumentu s.

Aby uzyskać więcej informacji, zobacz Jak zaimplementować i wywołać niestandardową metodę rozszerzenia.

Ogólnie rzecz biorąc, prawdopodobnie będziesz wywoływać metody rozszerzenia znacznie częściej niż implementowanie własnych. Metody rozszerzenia są wywoływane przy użyciu składni metody wystąpienia, więc nie jest potrzebna specjalistyczna wiedza, aby móc używać ich z poziomu kodu klienta. Aby włączyć metody rozszerzeń dla określonego typu, wystarczy dodać dyrektywę using dla przestrzeni nazw, w której są zdefiniowane metody. Aby na przykład użyć standardowych operatorów zapytań, dodaj tę using dyrektywę do kodu:

using System.Linq;

(Może być również konieczne dodanie odwołania do System.Core.dll). Zauważysz, że standardowe operatory zapytań są teraz wyświetlane w funkcji IntelliSense jako dodatkowe metody dostępne dla większości IEnumerable<T> typów.

Metody rozszerzające w czasie kompilacji

Można stosować metody rozszerzenia, aby rozszerzyć klasę lub interfejs, ale nie w celu pominięcia go. Metoda rozszerzenia mająca taką samą nazwę i podpis jak interfejs lub metoda klasy nigdy nie zostanie wywołana. W czasie kompilacji metody rozszerzenia zawsze mają niższy priorytet niż zdefiniowane w typie metody wystąpienia. Innymi słowy, jeśli typ ma metodę o nazwie Process(int i)i masz metodę rozszerzenia z tym samym podpisem, kompilator zawsze będzie wiązać się z metodą wystąpienia. Gdy kompilator napotyka wywołanie metody, najpierw szuka dopasowania w metodach wystąpienia danego typu. Jeśli nie zostanie znalezione dopasowanie, wyszukuje metody rozszerzeń zdefiniowane dla typu i wiążą się z pierwszą znalezioną metodą rozszerzenia.

Przykład

W poniższym przykładzie przedstawiono reguły, zgodnie z którymi kompilator języka C# określa, czy należy powiązać wywołanie metody z metodą wystąpienia typu, czy z metodą rozszerzenia. Klasa Extensions statyczna zawiera metody rozszerzeń zdefiniowane dla dowolnego typu, który implementuje IMyInterfaceelement . Klasy A, Bi C wszystkie implementują interfejs.

Metoda MethodB rozszerzenia nigdy nie jest wywoływana, ponieważ jej nazwa i podpis dokładnie pasują do metod, które zostały już zaimplementowane przez klasy.

Gdy kompilator nie może znaleźć metody wystąpienia z pasującym podpisem, będzie wiązać się z zgodną metodą rozszerzenia, jeśli istnieje.

// 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()
 */

Typowe wzorce użycia

Funkcja kolekcji

W przeszłości często tworzyło się "Klasy kolekcji", które implementowali System.Collections.Generic.IEnumerable<T> interfejs dla danego typu i zawierały funkcje, które działały na kolekcjach tego typu. Chociaż nie ma nic złego w tworzeniu tego typu obiektu kolekcji, te same funkcje można osiągnąć za pomocą rozszerzenia w obiekcie System.Collections.Generic.IEnumerable<T>. Rozszerzenia mają zaletę zezwalania na wywoływanie funkcji z dowolnej kolekcji, takiej jak System.Array lub System.Collections.Generic.List<T> , która implementuje System.Collections.Generic.IEnumerable<T> ten typ. Przykład użycia tablicy int32 można znaleźć we wcześniejszej części tego artykułu.

Funkcje specyficzne dla warstwy

W przypadku korzystania z architektury cebuli lub innego projektu aplikacji warstwowej często istnieje zestaw jednostek domeny lub obiektów transferu danych, których można używać do komunikacji między granicami aplikacji. Te obiekty zwykle nie zawierają żadnych funkcji ani tylko minimalnych funkcji, które mają zastosowanie do wszystkich warstw aplikacji. Metody rozszerzeń mogą służyć do dodawania funkcji specyficznych dla każdej warstwy aplikacji bez ładowania obiektu w dół z metodami niewymaganymi lub poszukiwanymi w innych warstwach.

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

Rozszerzanie wstępnie zdefiniowanych typów

Zamiast tworzyć nowe obiekty, gdy należy utworzyć funkcje wielokrotnego użytku, często możemy rozszerzyć istniejący typ, taki jak typ platformy .NET lub CLR. Jeśli na przykład nie używamy metod rozszerzeń, możemy utworzyć klasę Engine lub Query wykonującą zapytanie w programie SQL Server, które może być wywoływane z wielu miejsc w naszym kodzie. Zamiast tego możemy rozszerzyć klasę System.Data.SqlClient.SqlConnection przy użyciu metod rozszerzeń, aby wykonać to zapytanie z dowolnego miejsca, z którego mamy połączenie z programem SQL Server. Innymi przykładami mogą być dodanie do klasy typowych funkcji System.String , rozszerzenie możliwości System.IO.Stream przetwarzania danych obiektu i System.Exception obiektów na potrzeby określonej funkcji obsługi błędów. Tego typu przypadki użycia są ograniczone tylko przez wyobraźnię i dobre poczucie.

Rozszerzanie wstępnie zdefiniowanych typów może być trudne, struct ponieważ są przekazywane przez wartość do metod. Oznacza to, że wszelkie zmiany w strukturę są wprowadzane do kopii struktury. Te zmiany nie są widoczne po zakończeniu metody rozszerzenia. Modyfikator można dodać do pierwszego argumentu ref , który czyni go ref metodą rozszerzenia. Słowo ref kluczowe może pojawić się przed lub po słowie this kluczowym bez żadnych różnic semantycznych. ref Dodanie modyfikatora wskazuje, że pierwszy argument jest przekazywany przez odwołanie. Dzięki temu można pisać metody rozszerzeń, które zmieniają stan rozszerzonej struktury (należy pamiętać, że prywatne elementy członkowskie nie są dostępne). Tylko typy wartości lub typy ogólne ograniczone do struktury (zobacz struct ograniczenie , aby uzyskać więcej informacji) są dozwolone jako pierwszy parametr ref metody rozszerzenia. W poniższym przykładzie pokazano, jak użyć ref metody rozszerzenia, aby bezpośrednio zmodyfikować typ wbudowany bez konieczności ponownego przypisania wyniku lub przekazania go przez funkcję za pomocą słowa kluczowego 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
    }
}

W następnym przykładzie ref przedstawiono metody rozszerzenia dla typów struktur zdefiniowanych przez użytkownika:

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

Ogólne wskazówki

Mimo że nadal uważa się, że lepiej jest dodać funkcjonalność, modyfikując kod obiektu lub wyprowadzając nowy typ zawsze, gdy jest to uzasadnione i możliwe, metody rozszerzeń stały się kluczową opcją tworzenia funkcji wielokrotnego użytku w całym ekosystemie platformy .NET. W takich przypadkach, gdy oryginalne źródło nie jest pod kontrolą, gdy obiekt pochodny jest nieodpowiedni lub niemożliwy lub gdy funkcjonalność nie powinna być uwidoczniona poza odpowiednim zakresem, metody rozszerzenia są doskonałym wyborem.

Aby uzyskać więcej informacji na temat typów pochodnych, zobacz Dziedziczenie.

W przypadku używania metody rozszerzenia w celu rozszerzenia typu, którego kod źródłowy nie jest pod kontrolą, istnieje ryzyko, że zmiana w implementacji typu spowoduje przerwanie metody rozszerzenia.

W przypadku implementowania metod rozszerzeń dla danego typu należy pamiętać o następujących kwestiach:

  • Metoda rozszerzenia nie jest wywoływana, jeśli ma ten sam podpis co metoda zdefiniowana w typie.
  • Metody rozszerzenia są włączane do zakresu na poziomie przestrzeni nazw. Jeśli na przykład masz wiele klas statycznych, które zawierają metody rozszerzenia w jednej przestrzeni nazw o nazwie Extensions, wszystkie zostaną wprowadzone do zakresu przez dyrektywę using Extensions; .

Dla zaimplementowanej biblioteki klas nie należy używać metod rozszerzenia, aby uniknąć zwiększenia numeru wersji zestawu. Jeśli chcesz dodać znaczącą funkcjonalność do biblioteki, dla której jesteś właścicielem kodu źródłowego, postępuj zgodnie z wytycznymi platformy .NET dotyczącymi przechowywania wersji zestawu. Aby uzyskać więcej informacji, zobacz Przechowywanie wersji zestawu.

Zobacz też