Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Członkowie rozszerzenia pozwalają na "dodawanie" metod do tych istniejących typów bez tworzenia nowego typu pochodnego, ponownej kompilacji lub modyfikowania oryginalnego typu.
Począwszy od języka C# 14, istnieją dwie składnie używane do definiowania metod rozszerzeń. Język C# 14 dodaje extension
kontenery, w których można zdefiniować wiele metod rozszerzających zarówno dla typu, jak i dla jego instancji. Przed C# w wersji 14, należy dodać modyfikator this
do pierwszego parametru metody statycznej, aby wskazać, że metoda pojawia się jako członek instancji typu parametru.
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. Obie formy metod rozszerzeń są kompilowane w tym samym języku IL (język pośredni). Użytkownicy metod rozszerzeń nie muszą wiedzieć, która składnia została użyta do ich definiowania.
Najczęściej używane elementy członkowskie rozszerzenia to standardowe operatory zapytań LINQ, które dodają funkcjonalność zapytań do istniejących typów System.Collections.IEnumerable i System.Collections.Generic.IEnumerable<T>. Aby użyć standardowych operatorów zapytań, najpierw umieść je w zasięgu za pomocą dyrektywy 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 jest wyrażeniem lambda. Wiele standardowych operatorów zapytań bierze wyrażenia lambda jako parametry. Aby uzyskać więcej informacji, zapoznaj się z wyrażeniami lambda.
int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
Console.Write(i + " ");
}
//Output: 10 15 21 26 39 45
Metody rozszerzenia są definiowane jako metody statyczne, ale są wywoływane przy użyciu składni metody 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
.
Deklarowanie członków rozszerzenia
Począwszy od języka C# 14, można zadeklarować bloki rozszerzeń. Blok rozszerzenia to blok w klasie statycznej bez zagnieżdżenia, która zawiera elementy członkowskie rozszerzenia dla typu lub wystąpienia tego typu. Poniższy przykład kodu definiuje blok rozszerzenia dla string
typu. Blok rozszerzenia zawiera jeden element członkowski: metodę, która zlicza wyrazy w ciągu:
namespace CustomExtensionMembers;
public static class MyExtensions
{
extension(string str)
{
public int WordCount() =>
str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}
}
Przed użyciem języka C# 14 należy zadeklarować metodę rozszerzenia, dodając this
modyfikator do pierwszego parametru:
namespace CustomExtensionMethods;
public static class MyExtensions
{
public static int WordCount(this string str) =>
str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}
Obie formy rozszerzeń muszą być zdefiniowane wewnątrz nienagnieżdżonej, niegenerowanej klasy statycznej.
Można ją wywołać z aplikacji przy użyciu składni umożliwiającej dostęp do członków instancji:
string s = "Hello Extension Methods";
int i = s.WordCount();
Podczas gdy składowe rozszerzenia dodają nowe funkcje do istniejącego typu, nie naruszają zasady hermetyzacji. Deklaracje dostępu dla wszystkich elementów rozszerzonego typu odnoszą się do składowych jego rozszerzenia.
Zarówno klasa MyExtensions
, jak i metoda WordCount
są static
, i można uzyskać do nich dostęp tak, jak do wszystkich innych elementów członkowskich static
. 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);
Powyższy kod języka C# ma zastosowanie zarówno do bloku rozszerzenia, jak i składni członków rozszerzenia this
. Poprzedni kod:
- Deklaruje i przypisuje nową
string
nazwę o nazwies
z wartością"Hello Extension Methods"
. - Wywołania
MyExtensions.WordCount
podanego argumentus
.
Aby uzyskać więcej informacji, zobacz Jak zaimplementować i wywołać niestandardową metodę rozszerzenia.
Ogólnie rzecz biorąc, prawdopodobnie wywołujesz elementy członkowskie rozszerzenia znacznie częściej niż implementujesz je. Ponieważ składowe rozszerzeń są wywoływane tak, jakby zostały zadeklarowane jako elementy członkowskie klasy rozszerzonej, nie jest wymagana specjalna wiedza, aby używać ich z kodu klienta. Aby włączyć członków rozszerzenia dla określonego typu, wystarczy dodać dyrektywę using
do przestrzeni nazw, w której zdefiniowano metody. Aby na przykład użyć standardowych operatorów zapytań, dodaj tę using
dyrektywę do kodu:
using System.Linq;
Wiązanie członków rozszerzenia w czasie kompilacji
Można użyć składowych rozszerzeń, aby rozszerzyć klasę lub interfejs, ale nie zastąpić zachowania zdefiniowanego w klasie. Składowa rozszerzenia o tej samej nazwie i podpisie co interfejs lub składowe klasy nigdy nie są wywoływane. W czasie kompilacji elementy rozszerzeń zawsze mają niższy priorytet niż elementy instancji (lub statyczne) zdefiniowane w samym typie. Innymi słowy, jeśli typ ma metodę o nazwie Process(int i)
i masz metodę rozszerzenia z tym samym podpisem, kompilator zawsze wiąże się z metodą składową. Gdy kompilator napotka wywołanie elementu członkowskiego, najpierw wyszukuje dopasowanie w elementach członkowskich typu. Jeśli nie zostanie znalezione dopasowanie, wyszukuje wszystkie elementy członkowskie rozszerzenia zdefiniowane dla typu. Wiąże się z pierwszym członkiem rozszerzenia, który znajdzie. Poniższy przykład pokazuje reguły, którymi kieruje się kompilator języka C# w określaniu, czy powiązać członka instancji z typem, czy z członkiem rozszerzenia. Statyczna klasa Extensions
zawiera składowe rozszerzenia zdefiniowane dla dowolnego typu, który implementuje IMyInterface
:
public interface IMyInterface
{
void MethodB();
}
// Define extension methods for IMyInterface.
// 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)");
}
Równoważne rozszerzenia można zadeklarować przy użyciu składni składowej rozszerzenia języka C# 14:
public static class Extension
{
extension(IMyInterface myInterface)
{
public void MethodA(int i) =>
Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");
public void MethodA(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 void MethodB() =>
Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
}
}
Klasy A
, B
i C
wszystkie implementują interfejs:
// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
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)");
}
}
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, wiąże się z zgodną metodą rozszerzenia, jeśli istnieje.
// 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 implementowały System.Collections.Generic.IEnumerable<T> interfejs dla danego typu i zawierały funkcjonalność, która działała 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.
Layer-Specific funkcjonalność
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.
public class DomainEntity
{
public int Id { get; set; }
public required string FirstName { get; set; }
public required string LastName { get; set; }
}
static class DomainEntityExtensions
{
static string FullName(this DomainEntity value)
=> $"{value.FirstName} {value.LastName}";
}
Można zadeklarować równoważną FullName
właściwość w języku C# 14 lub nowszym przy użyciu nowej składni bloku rozszerzenia:
static class DomainEntityExtensions
{
extension(DomainEntity value)
{
string FullName => $"{value.FirstName} {value.LastName}";
}
}
Rozszerzanie wstępnie zdefiniowanych typów
Zamiast tworzyć nowe obiekty, gdy trzeba utworzyć funkcje wielokrotnego użytku, często można rozszerzyć istniejący typ, taki jak typ platformy .NET lub CLR. Jeśli na przykład nie używasz metod rozszerzeń, możesz utworzyć klasę Engine
lub Query
, aby wykonać pracę wykonującą zapytanie w programie SQL Server, które może być wywoływane z wielu miejsc w naszym kodzie. Zamiast tego można rozszerzyć klasę System.Data.SqlClient.SqlConnection przy użyciu metod rozszerzeń, aby wykonać to zapytanie z dowolnego miejsca, w którym masz 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, takich jak struct
, może być trudne, ponieważ są one przekazywane do metod przez wartość. Oznacza to, że wszelkie zmiany w strukturze są wprowadzane do kopii struktury. Te zmiany nie są widoczne po zakończeniu działania metody rozszerzenia. Można dodać modyfikator ref
do pierwszego argumentu, tworząc w ten sposób metodę rozszerzenia ref
. 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. Ta technika umożliwia pisanie metod 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 z ograniczeniem do struktury (więcej informacji na temat tych reguł znajdziesz w sekcji struct
ograniczeń) są dozwolone jako pierwszy parametr w metodzie rozszerzenia ref
lub jako odbiorca bloku 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++;
}
Równoważne bloki rozszerzenia są wyświetlane w następującym kodzie:
public static class IntExtensions
{
extension(int number)
{
public void Increment()
=> number++;
}
// Take note of the extra ref keyword here
extension(ref int number)
{
public void RefIncrement()
=> number++;
}
}
Różne bloki rozszerzeń są wymagane do odróżnienia trybów parametrów by-value i by-ref dla odbiornika.
Możesz zobaczyć różnicę, jaką zastosowanie ref
do odbiornika ma w poniższym przykładzie:
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
Tę samą technikę można zastosować, dodając ref
elementy członkowskie rozszerzenia do 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
}
}
Powyższy przykład można również utworzyć przy użyciu bloków rozszerzeń w języku C# 14:
public static class AccountExtensions
{
extension(ref Account account)
{
// ref keyword can also appear before the this keyword
public void Deposit(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
}
}
}
Dostęp do tych metod rozszerzeń można uzyskać w następujący sposób:
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 wytyczne
Lepiej jest dodać funkcjonalność, modyfikując kod obiektu lub wyprowadzając nowy typ, gdy jest to uzasadnione i możliwe. Metody rozszerzeń to kluczowa opcja tworzenia funkcji wielokrotnego użytku w ekosystemie platformy .NET. Elementy członkowskie rozszerzeń są preferowane, gdy nie masz kontroli nad oryginalnym źródłem, gdy użycie obiektu pochodnego jest niewłaściwe lub niemożliwe, albo gdy zakres funkcjonalności jest ograniczony.
Aby uzyskać więcej informacji na temat typów pochodnych, zobacz Dziedziczenie.
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 rozszerzeń są wprowadzane w zakres 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 z nich są wprowadzane do zakresu przez dyrektywęusing Extensions;
.
W przypadku zaimplementowanej biblioteki klas nie należy używać metod rozszerzeń, 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 Wersjonowanie zestawu.
Zobacz też
- Przykłady programowania równoległego (wiele przykładów demonstruje metody rozszerzenia)
- Wyrażenia lambda
- Standardowe operatory zapytań — omówienie
- Reguły konwersji parametrów wystąpienia i ich wpływ
- Współdziałanie metod rozszerzeń między językami
- Metody rozszerzeń i delegaty curried
- Powiązanie metody rozszerzenia i raportowanie błędów