Методы расширения (руководство по программированию в C#)
Обновлен: Ноябрь 2007
Метода расширения позволяют "добавлять" методы в существующие типы без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Методы расширения являются особым видом статического метода, но они вызываются, как если бы они были методами экземпляра в расширенном типе. Для клиентского кода, написанного на языках C# и Visual Basic, нет видимого различия между вызовом метода расширения и вызовом методов, фактически определенных в типе.
Наиболее распространенными методами расширения являются стандартные операторы запроса LINQ, добавляющие функции запроса в существующие типы System.Collections.IEnumerable и System.Collections.Generic.IEnumerable<T>. Для использования стандартных операторов запроса их сначала надо добавить в область видимости с помощью директивы using System.Linq. Затем каждый тип, который реализует тип IEnumerable<T>, будет иметь методы экземпляра, такие как GroupBy, OrderBy, Average и т.д. Эти дополнительные методы можно видеть в завершении операторов IntelliSense, когда вводится точка после экземпляра типа IEnumerable<T>, например List<T> или Array.
В следующем примере показано, как вызывать метод стандартного оператора запроса OrderBy для массива целых чисел. Выражение в скобках называется лямбда-выражением. Многие стандартные операторы запроса принимают лямбда-выражения в качестве параметров, но это не является обязательным для методов расширения. Дополнительные сведения см. в разделе Лямбда-выражения (Руководство по программированию в C#).
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
Методы расширения определяются как статические методы, но вызываются с помощью синтаксиса обращения к методу экземпляра. Их первый параметр определяет, с каким типом оперирует метод, и перед параметром идет модификатор this. Методы расширения находятся в области действия, только если пространство имен было явно импортировано в исходный код с помощью директивы using.
В приведенном ниже примере показан метод расширения, определяемый для класса System.String. Обратите внимание, что этот метод определяется внутри не вложенного, не универсального статического класса.
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
Метод расширения WordCount может быть добавлен в область действия с помощью директивы using.
using ExtensionMethods;
И он может вызываться из приложения с помощью следующего синтаксиса.
string s = "Hello Extension Methods";
int i = s.WordCount();
В созданном коде метод расширения вызывается с помощью синтаксиса обращения к метода экземпляра. Однако промежуточный язык (IL), генерируемый компилятором, переводит созданный код в вызов статического метода. Поэтому принцип инкапсуляции фактически не нарушается. В действительности, методы расширения не могут получить доступ к закрытым переменным типа, для расширения которого они используются.
Дополнительные сведения см. в разделе Практическое руководство. Реализация и вызов пользовательского метода расширения (Руководство по программированию в C#).
Вообще, обычно гораздо чаще приходится вызывать методы расширения, чем реализовывать собственные методы. Так как методы расширения вызываются с помощью синтаксиса обращения к методу экземпляра, для использования их из клиентского кода специальные знания не требуются. Чтобы включить методы расширения для определенного типа необходимо просто добавить директиву using для пространства имен, в котором эти методы определяются. Например, чтобы использовать стандартные операторы запроса, необходимо добавить директиву using в создаваемый код.
using System.Linq;
(Также может потребоваться добавить ссылку на библиотеку System.Core.dll.) Обратите внимание, что стандартные операторы запроса теперь появляются в списке IntelliSense в виде дополнительных методов, доступных для большинства типов IEnumerable<T>.
Примечание. |
---|
Хотя стандартные операторы запросов не появляются в списке IntelliSense для типа String, они все равно доступны. |
Привязка методов расширения во время компиляции
Методы расширения можно использовать для расширения класса или интерфейса, но не для их переопределения. Метод расширения, имеющий то же имя и подпись, что и интерфейс или метод класса, никогда не вызывается. Во время компиляции методы расширения всегда имеют более низкий приоритет, чем методы экземпляра, определенные в самом типе. Другими словами, если тип имеет метод Process(int i), а также есть метод расширения с такой же подписью, компилятор будет всегда выполнять привязку к методу экземпляра. Если компилятор обнаруживает вызов метода, то сначала выполняется поиск совпадения с методами экземпляра типа. Если такое совпадение не найдено, компилятор выполняет поиск методов расширения, определенных для соответствующего типа, и делает привязку к первому найденному методу расширения. В следующем примере кода демонстрируется, как компилятор определяет, к какому методу расширения или методу экземпляра необходимо выполнить привязку.
Пример
В следующем примере демонстрируются правила, которые компилятор C# соблюдает при определении того, к чему необходимо привязать вызов метода — к методу экземпляра типа или к методу расширения. Статический класс Extensions содержит методы расширения, определяемые для любого типа, реализующего интерфейс IMyInterface. Все три класса — A, B и C — реализуют этот интерфейс.
Метод MethodB никогда не вызывается, потому что его имя и подпись точно совпадают с методами, уже реализованными этими классами.
Когда компилятор не может найти метод экземпляра с совпадающей подписью, он выполняет привязку к совпадающему методу расширения, если такой существует.
namespace Extensions
{
using System;
using ExtensionMethodsDemo1;
// Define extension methods for any type 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, because the three classes implement MethodB.
public static void MethodB(this IMyInterface myInterface)
{
Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
}
}
}
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
public interface IMyInterface
{
void MethodB();
}
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)
{
A a = new A();
B b = new B();
C c = new C();
TestMethodBinding(a,b,c);
}
static void TestMethodBinding(A a, B b, C c)
{
// A has no methods, so each call resolves to
// the extension methods whose signatures match.
a.MethodA(1); // Extension.MethodA(object, int)
a.MethodA("hello"); // Extension.MethodA(object, string)
a.MethodB(); // A.MethodB()
// B itself has a method with this signature.
b.MethodA(1); // B.MethodA(int)
b.MethodB(); // B.MethodB()
// B has no matching method, but Extension does.
b.MethodA("hello"); // Extension.MethodA(object, string)
// In each case C has a matching instance method.
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()
*/
Общие рекомендации
В общем, рекомендуется реализовывать методы расширения ограниченно, только когда это необходимо. Когда это возможно, клиентский код, который используется для расширения существующего типа, должен осуществлять расширение путем создания нового типа, производного от существующего. Дополнительные сведения см. в разделе Наследование (Руководство по программированию в C#).
При использовании метода расширения для расширения типа, исходный код которого невозможно изменить, возникает риск того, что изменение в реализации типа вызовет сбой метода расширения.
В случае реализации методов расширения для какого-либо типа необходимо помнить о следующих двух фактах.
Метод расширения никогда не будет вызван, если он имеет ту же самую подпись, что и метод, определенный в типе.
Методы расширения добавляются в область действия на уровне пространства имен. Например, при наличии нескольких статических классов, которые содержат методы расширения в единственном пространстве имен с названием Extensions, все они будут добавлены в область действия директивной using Extensions;.
Разработчики библиотек классов не должны использовать методы расширения, чтобы избежать создания новых версий сборок. Если необходимо добавить существенные новые функции в библиотеку и разработчик владеет исходным кодом, то следует выполнять стандартные инструкции платформы .NET Framework по управлению версиями сборок. Дополнительные сведения см. в разделе Управление версиями сборок.
См. также
Основные понятия
Руководство по программированию в C#
Общие сведения о стандартных операторах запроса
Ссылки
Лямбда-выражения (Руководство по программированию в C#)
Другие ресурсы
Правила преобразования для параметров экземпляров и их влияние