Menu

Методы расширения в C#: синтаксис, IEnumerable, LINQ и правила

Как работают методы расширения в C#: статический метод в статическом классе с this перед первым параметром, вызов как у экземплярного метода, расширение string, перечислений и IEnumerable<T>, как на них построен LINQ, почему побеждают экземплярные методы, вызов на null и нужная директива using.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Метод расширения добавляет метод к существующему типу, не изменяя его, не наследуясь от него и не оборачивая его. Вы пишете обычный статический метод, помечаете его первый параметр словом this, а вызывающий код использует его через точку, как будто у типа всегда был такой метод:

Вывод:

Extension methods let yo...
12
short
Extension...

title.Truncate(24) компилируется ровно в StringExtensions.Truncate(title, 24). Синтаксис с точкой это лишь удобство: метод по-прежнему статический, по-прежнему находится в StringExtensions и может использовать только открытые члены string.

Правила объявления

  • Метод должен быть static и находиться в классе, который static, не обобщённый и не вложен в другой класс. Нестатический или обобщённый класс даёт CS1106, Extension method must be defined in a non-generic static class; вложенный даёт CS1109, Extension methods must be defined in a top level static class.
  • this ставится только перед первым параметром, и тип этого параметра это расширяемый тип. Остальные параметры обычные.
  • Сам метод может быть обобщённым (this IEnumerable<T> source), хотя класс не может.
  • Он видит только то, что видит любой внешний код: открытые (и в пределах той же сборки внутренние) члены. Закрытые поля остаются закрытыми.

По соглашению класс называют по тому, что он расширяет (StringExtensions, EnumerableExtensions), и группируют такие классы в пространстве имён вроде MyApp.Extensions.

Расширение интерфейсов и IEnumerable<T>

Расширение интерфейса добавляет метод каждому типу, который его реализует. Поэтому расширение IEnumerable<T> сразу даёт метод массивам, спискам, множествам, коллекциям ключей словарей и результатам запросов LINQ:

Вывод:

9.5
50
65
f0 f3 f6

EveryNth обобщённый: T выводится из получателя, поэтому метод работает здесь со строками и с любым другим типом элементов. Поскольку он использует yield return, он ленивый, как встроенные операторы LINQ, и его можно объединять с ними в цепочки.

LINQ построен на методах расширения

Where, Select, OrderBy, Sum, First и остальной LINQ это методы расширения для IEnumerable<T>, определённые в статическом классе System.Linq.Enumerable. Поэтому using System.Linq; должен стоять в начале файла, прежде чем list.Where(...) скомпилируется, и поэтому LINQ работает с любым типом коллекции, хотя ни один из них не реализует метод Where:

Вывод:

72, 88, 95
72, 88, 95

Эти две строки это один и тот же код. Синтаксис расширений превращает вложенные вызовы, которые читаются изнутри наружу, в цепочку, которая читается в порядке выполнения шагов, и это главная причина существования методов расширения: они появились в C# 3 вместе с LINQ. Операторы описаны на странице о LINQ.

Пространство имён должно быть импортировано

Метод расширения доступен, только когда пространство имён его статического класса импортировано через using. Без этого вызов завершается ошибкой, как будто метода не существует:

error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

Добавьте using для пространства имён расширений (или, начиная с C# 10, один раз global using на весь проект):

Вывод:

EUR 49.90

Обратная сторона полезна: метод расширения, который не нужен везде, может жить в пространстве имён, которое импортируют лишь некоторые файлы.

Экземплярные методы побеждают

Когда у типа уже есть подходящий экземплярный метод, компилятор использует его и никогда не рассматривает расширение. Расширение с той же сигнатурой становится мёртвым кодом в каждом месте вызова через точку:

Вывод:

Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call

Расширение с одним аргументом проиграло экземплярному методу; расширение с двумя аргументами было использовано, потому что ни один экземплярный метод не принимает строку. Это также означает, что обновление библиотеки, добавившее экземплярный метод с именем вашего расширения, молча переключит ваши вызовы на версию библиотеки. Выбирайте для расширений имена, которые вряд ли совпадут.

Вызов на null

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

Вывод:

True
AL
ArgumentNullException: fullName

То, что missing.IsBlank() возвращает true, удобно, но может удивить читателей, которые ожидают, что вызов члена на null выбросит исключение. Принимайте null только в методах, по имени которых это очевидно (IsBlank, OrEmpty); в остальных случаях выбрасывайте ArgumentNullException, как это делает LINQ.

Расширение перечислений

У перечислений не может быть собственных методов, поэтому они естественная цель для расширений:

enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

static class OrderStatusExtensions
{
    public static bool IsFinal(this OrderStatus s) =>
        s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}

// usage: if (order.Status.IsFinal()) { ... }

Члены расширения в C# 14

До C# 14 расширениями могли быть только методы. C# 14 добавил блоки extension, которые группируют члены для одного получателя и допускают также свойства расширения и статические члены:

// C# 14 and later
public static class StringExtensions
{
    extension(string s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // extension property
        public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
    }
}

Классические методы расширения с параметром this остаются допустимыми, и именно их использует почти весь существующий код и библиотеки.

Когда их писать

Методы расширения подходят, когда тип не ваш (string, DateTime, интерфейсы фреймворка), когда нужен один и тот же вспомогательный метод для всех реализаций интерфейса или когда цепочка читается лучше вложенных вызовов. Если класс ваш, добавьте настоящий экземплярный метод: он может использовать закрытое состояние и находится там, где его ищут читатели. И избегайте расширения object, которое помещает ваш метод в список автодополнения для каждого значения в программе.

Часто задаваемые вопросы

Что такое метод расширения в C#?

Статический метод, который можно вызывать так, будто это экземплярный метод другого типа. Вы пишете его в статическом классе и ставите this перед первым параметром: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. После этого name.IsBlank() работает для любой строки без изменения string и без наследования от него.

Почему мой метод расширения не находится?

Методы расширения видны, только когда их пространство имён импортировано. Если статический класс находится в MyApp.Extensions, добавьте using MyApp.Extensions; в файл, который его вызывает; иначе компилятор сообщит CS1061: у типа нет определения и нет доступного метода расширения с таким именем. Также проверьте, что класс static, не обобщённый и не вложенный.

Может ли метод расширения переопределить экземплярный метод?

Нет. Компилятор сначала ищет подходящий экземплярный метод и рассматривает методы расширения, только если такого нет. Расширение с тем же именем и параметрами, что и у экземплярного метода, молча игнорируется в каждом месте вызова с синтаксисом экземпляра.

Можно ли вызвать метод расширения на null в C#?

Да. Вызов компилируется в вызов статического метода с null в качестве первого аргумента, поэтому на самом вызове NullReferenceException не возникает. Что делать, решает метод: считать null допустимым входом (как вспомогательный метод в стиле IsNullOrEmpty) или выбросить ArgumentNullException, как это делают методы LINQ.

Есть ли в C# свойства расширения?

Не до C# 14. До этого расширениями могли быть только методы, поэтому то, что хотелось бы сделать свойством, пишется как метод (GetFullName()). C# 14 добавил блоки расширения, которые могут объявлять свойства расширения и статические члены, а не только методы.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ