Прочитать на английском

Поделиться через


Члены расширения (руководство по программированию на C#)

Члены расширения позволяют добавлять методы в существующие типы без создания нового производного типа, перекомпиляции или изменения исходного типа.

Начиная с C# 14, существует два синтаксиса, которые используются для определения методов расширения. C# 14 добавляет extension контейнеры, в которых определяется несколько членов расширения для типа или экземпляра типа. Перед C# 14 добавьте this модификатор к первому параметру статического метода, чтобы указать, что метод отображается как член экземпляра типа параметра.

Методы расширения являются статическими методами, но они вызываются так же, как если бы они были методами экземпляра в расширенном типе. Для клиентского кода, написанного на C#, F# и Visual Basic, нет видимой разницы между вызовом метода расширения и методами, определенными в типе. Обе формы методов расширения компилируются в один и тот же IL (промежуточный язык). Потребители членов расширения не должны знать, какой синтаксис использовался для определения методов расширения.

Наиболее распространенными элементами расширения являются стандартные операторы запросов LINQ, добавляющие функции запросов к существующим System.Collections.IEnumerable и System.Collections.Generic.IEnumerable<T> типам. Чтобы использовать стандартные операторы запросов, сначала подключите их с помощью директивы using System.Linq. Затем любой тип, реализующий IEnumerable<T>, по-видимому, имеет такие методы экземпляра, как GroupBy, OrderBy, Average и т. д. Эти дополнительные методы можно увидеть в завершении инструкции IntelliSense, когда вы вводите "точка" после экземпляра типа IEnumerable<T>, например List<T> или Array.

Пример метода OrderBy

В следующем примере показано, как вызвать стандартный метод оператора OrderBy запроса в массив целых чисел. Выражение в скобках является лямбда-выражением. Многие стандартные операторы запросов принимают лямбда-выражения в качестве параметров. Дополнительные сведения см. в разделе Лямбда-выражения.

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

Методы расширения определяются как статические методы, но вызываются с помощью синтаксиса метода экземпляра. Их первый параметр указывает тип, с которым работает метод. Параметр следует этому модификатору. Методы расширения доступны только при явном импорте пространства имен в ваш исходный код с использованием директивы using.

Объявление элементов расширения

Начиная с C# 14, можно объявить блоки расширения. Блок расширения — это блок в неложенном, негенерическом, статичном классе, который содержит члены расширения для типа или экземпляра этого типа. В следующем примере кода определяется блок расширения для string типа. Блок расширения содержит один элемент: метод, который подсчитывает слова в строке:

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

Перед C# 14 вы объявляете метод расширения путем добавления this модификатора в первый параметр:

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

Обе формы расширений должны быть определены внутри невложенного негенерированного статического класса.

И его можно вызвать из приложения с помощью синтаксиса для доступа к членам экземпляра:

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

Хотя члены расширения добавляют новые возможности в существующий тип, члены расширения не нарушают принцип инкапсуляции. Объявления доступа для всех членов расширенного типа применяются к членам расширения.

Класс MyExtensions и метод WordCount являются static и к ним можно получить доступ, как и ко всем остальным членам static. Метод WordCount можно вызвать так же, как и другие методы static, следующим образом:

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

Предыдущий код C# применяется как к блоку расширения, так и к синтаксису this для членов расширения. Предыдущий код:

  • Объявляет и присваивает новый string с именем s и значением "Hello Extension Methods".
  • Вызывает функцию MyExtensions.WordCount с заданным аргументом s.

Дополнительные сведения см. в статье о реализации и вызове пользовательского метода расширения.

Как правило, вы вызываете члены расширения функций гораздо чаще, чем реализуете их. Поскольку члены расширения вызываются так, будто они объявлены как члены расширяемого класса, специальных знаний для их использования в клиентском коде не требуется. Чтобы включить члены расширения для определенного типа, достаточно добавить директиву using для пространства имен, в котором определены методы. Например, чтобы использовать стандартные операторы запросов, добавьте эту using директиву в код:

using System.Linq;

Привязка членов расширения во время компиляции

Члены расширения можно использовать для расширения класса или интерфейса, но не для переопределения поведения, определенного в классе. Член расширения с тем же именем и подписью, что и у членов интерфейса или класса, никогда не вызывается. Во время компиляции члены расширения всегда имеют более низкий приоритет, чем элементы экземпляра (или статические), определенные в самом типе. Другими словами, если тип имеет метод с именем Process(int i), и у вас есть метод расширения с той же сигнатурой, компилятор всегда привязывается к методу-члену. Когда компилятор обнаруживает вызов элемента, он сначала ищет совпадение в членах типа. Если совпадение не найдено, он ищет какие-либо члены расширения, определенные для типа. Он привязывается к первому элементу расширения, который он находит. В следующем примере демонстрируются правила, которые компилятор C# использует при определении того, следует ли привязать к члену экземпляра данного типа или к члену расширения. Статический класс Extensions содержит члены расширения, определенные для любого типа, реализующего 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)");
}

Эквивалентные расширения можно объявить с помощью синтаксиса члена расширения 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)");
    }
}

Классы Aи BC все реализуют интерфейс:

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

MethodB Метод расширения никогда не вызывается, так как его имя и подпись точно соответствуют методам, уже реализованным классами. Если компилятор не может найти метод экземпляра с соответствующей сигнатурой, он привязывается к методу соответствующего расширения, если он существует.

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

Распространенные шаблоны использования

Функции коллекции

В прошлом было распространено создание классов коллекции, реализующих System.Collections.Generic.IEnumerable<T> интерфейс для заданного типа и содержащих функциональные возможности, которые действовали в коллекциях этого типа. Хотя при создании этого типа объекта коллекции нет ничего плохого, с помощью расширения на объекте System.Collections.Generic.IEnumerable<T>коллекции можно добиться одной и той же функциональности. Расширения имеют преимущество, позволяя вызывать функциональные возможности из любой коллекции, такой как System.Array или System.Collections.Generic.List<T>, которая реализует System.Collections.Generic.IEnumerable<T> для этого типа. Пример использования массива Int32 можно найти ранее в этой статье.

Layer-Specific Функциональные возможности

При использовании архитектуры onion или другого многоуровневого приложения обычно используется набор сущностей домена или объектов передачи данных, которые можно использовать для обмена данными между границами приложения. Эти объекты обычно не содержат функциональных возможностей или только минимальных функциональных возможностей, которые применяются ко всем слоям приложения. Методы расширения можно использовать для добавления функциональных возможностей, относящихся к каждому уровню приложений.

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

Можно объявить эквивалентное FullName свойство в C# 14 и более поздних версиях с помощью нового синтаксиса блока расширения:

static class DomainEntityExtensions
{
    extension(DomainEntity value)
    {
        string FullName => $"{value.FirstName} {value.LastName}";
    }
}

Расширение предопределенных типов

Вместо создания новых объектов при создании повторно используемых функциональных возможностей часто можно расширить существующий тип, например тип .NET или CLR. Например, если вы не используете методы расширения, можно создать Engine или Query класс для выполнения запроса на SQL Server, который может вызываться из нескольких мест в нашем коде. Однако вместо этого можно расширить System.Data.SqlClient.SqlConnection класс с помощью методов расширения для выполнения этого запроса в любом месте, где есть подключение к SQL Server. Другие примеры могут быть для добавления общих функций в System.String класс, расширения возможностей System.IO.Stream обработки данных объекта и System.Exception объектов для конкретных функций обработки ошибок. Эти типы вариантов использования ограничены только вашим воображением и хорошим смыслом.

Расширение предопределённых типов может быть затруднительно с типами struct, так как они передаются по значению в методы. Это означает, что любые изменения структуры вносятся в копию структуры. Эти изменения не отображаются после выхода метода расширения. Вы можете добавить модификатор ref к первому аргументу, что сделает его методом расширения ref. Ключевое ref слово может отображаться до или после ключевого this слова без каких-либо семантических различий. ref Добавление модификатора указывает, что первый аргумент передается по ссылке. Этот метод позволяет создавать методы расширения, изменяющие состояние расширенной структуры (обратите внимание, что частные члены недоступны). Только типы значений или универсальные типы, ограниченные структурой (дополнительные сведения об этих правилах, см struct . ограничение для получения дополнительных сведений) разрешены в качестве первого параметра ref метода расширения или в качестве получателя блока расширения. В следующем примере показано, как использовать ref метод расширения для непосредственного изменения встроенного типа без необходимости переназначить результат или передать его через функцию с 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 IntExtensions
{
    extension(int number)
    {
        public void Increment()
            => number++;
    }

    // Take note of the extra ref keyword here
    extension(ref int number)
    {
        public void RefIncrement()
            => number++;
    }
}

Различные блоки расширений требуются для различения режимов параметров по значению и по ссылке для приемника.

Вы можете увидеть разницу, которую применение ref к получателю имеет на следующем примере:

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

Вы можете применить тот же метод, добавив ref члены расширения в определяемые пользователем типы структур:

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

Предыдущий пример также можно создать с помощью блоков расширений в 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
        }
    }
}

Эти методы расширения можно получить следующим образом:

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

Общие рекомендации

Рекомендуется добавить функциональные возможности, изменив код объекта или создав новый тип всякий раз, когда это разумно и возможно. Методы расширения являются важным вариантом для создания повторно используемых функций во всей экосистеме .NET. Члены расширения предпочтительнее, если исходный источник не находится под вашим контролем, если производный объект является неуместным или невозможным, или если функциональность имеет ограниченную область.

Дополнительные сведения о производных типах см. в разделе "Наследование".

Если вы реализуете методы расширения для данного типа, помните следующие моменты:

  • Метод расширения не вызывается, если он имеет ту же сигнатуру, что и метод, определенный в типе.
  • Методы расширения попадают в область видимости на уровне пространства имен. Например, если у вас есть несколько статических классов, содержащих методы расширения в одном пространстве имен, Extensionsвсе из них попадают в область директивы using Extensions; .

Для реализованной библиотеки классов не следует использовать методы расширения, чтобы избежать увеличения числа версий сборки. Если вы хотите добавить значительные функциональные возможности в библиотеку, для которой принадлежит исходный код, следуйте рекомендациям .NET по управлению версиями сборок. Дополнительные сведения см. в разделе "Управление версиями сборок".

См. также


Дополнительные ресурсы

Обучение

Модуль

Реализация свойств и методов класса - Training

Узнайте, как реализовать свойства класса только для чтения, чтения и записи с помощью модификаторов доступа и доступа, а также как реализовать методы и методы расширения для класса.