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


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

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

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

Пример OrderBy

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

class ExtensionMethods2
{
    static void Main()
    {
        int[] ints = [10, 45, 15, 39, 21, 26];
        IOrderedEnumerable<int> result = ints.OrderBy(g => g);
        foreach (int i in result)
        {
            Console.Write(i + " ");
        }
    }
}
//Output: 10 15 21 26 39 45

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

В следующем примере показан метод расширения, определенный для класса System.String. Он определен внутри невложенного, не обобщённого статического класса.

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

Метод WordCount расширения можно включить в область действия с помощью этой using директивы:

using ExtensionMethods;

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

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

Вы вызываете метод расширения в коде, используя синтаксис метода экземпляра. Промежуточный язык (IL), созданный компилятором, преобразует код в вызов статического метода. Принцип инкапсуляции на самом деле не нарушается. Методы расширения не могут получить доступ к частным переменным в типе, который они расширяют.

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

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

Предыдущий код C#:

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

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

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

using System.Linq;

(Также может потребоваться добавить ссылку на System.Core.dll.) Вы заметите, что стандартные операторы запросов теперь отображаются в IntelliSense в качестве дополнительных методов, доступных для большинства IEnumerable<T> типов.

Методы расширения привязки во время компиляции

Методы расширения можно использовать для расширения класса или интерфейса, но не для их переопределения. Метод расширения с тем же именем и сигнатурой, что и интерфейс или метод класса, никогда не будет вызываться. Во время компиляции методы расширения всегда имеют более низкий приоритет, чем экземплярные методы, определенные в самом типе. Другими словами, если тип имеет метод с именем Process(int i)и имеет метод расширения с той же сигнатурой, компилятор всегда привязывается к методу экземпляра. Когда компилятор обнаруживает вызов метода, он сначала ищет совпадение в методах экземпляра типа. Если совпадение не найдено, он ищет методы расширения, определенные для типа, и привязывается к первому методу расширения, который он находит.

Пример

В следующем примере демонстрируются правила, которым следует компилятор C# для определения, следует ли привязать вызов метода к методу экземпляра данного типа или к методам расширения. Статический класс Extensions содержит методы расширения, определенные для любого типа, реализующего IMyInterface. Классы A, B и C все реализуют интерфейс.

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

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

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

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

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

В прошлом было распространено создание классов коллекции, реализующих 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 string FirstName { get; set; }
    public string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{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 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
    }
}

В следующем примере показаны 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
    }
}

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

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

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

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

При использовании метода расширения для расширения типа, исходный код которого не контролируется, возникает риск того, что изменение в реализации типа приведет к разрыву метода расширения.

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

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

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

См. также