Создание расширяющих методов на C#

Введение

Согласно MSDN, расширяющие методы позволяют “добавлять” методы в существующие типы без создания производного типа, рекомпиляции и внесения прочих изменений. Расширяющие методы статичны, но вызываются так, будто являются методами экземпляра в расширенном типе.

Звучит это все здорово, но давайте разберемся подробнее.

Расширяющие методы представляют прекрасный пример принципа открытости/закрытости, который гласит: “открыт для расширения, закрыт для изменения”. Программные сущности должны быть открыты для расширения, но закрыты для изменения. Проще говоря, сущность должна легко расширяться без внутреннего изменения.

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

Но как это происходит?

Мы не будем использовать очередной пример с палиндромами. Вместо этого мы реализуем что-нибудь полезное. Напишем расширяющий метод для получения описания enum по его значению.

Реализация

Начнем с основного: “Можно ли внести изменения в класс String?».

Конечно, нет. У вас нет возможности вносить какие-либо изменения в системные классы. Тут вы можете поспорить, что можно наследовать класс и расширить его дополнительной функциональностью, переопределив существующие методы. Однако эта опция также недоступна, поскольку это запечатанный класс, а значит для наследования он закрыт.

Взгляните на рис. 1  —  это общий план наших действий.

Рис. 1: расширяющие методы для enum и string

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

  • Первый, GetEnumDescription(), используется в типе enum для получения Description значений перечисления.
  • Второй, GetEnumValueByDescription(), используется в типе string для получения значений enum по их описанию.

Теперь создайте перечисление, как показано в листинге 1 ниже. Это будет перечисление флагманов смартфонов.

Листинг 1. FlagshipSmartphone.cs:

public enum FlagshipSmartphone
{
[Description("iPhone 13 Pro Max")]
Apple,
[Description("Samsung Galaxy Note 20")]
Samsung,
[Description("OnePlus 9 Pro")]
OnePlus,
[Description("Google Pixel 6 Pro")]
Google
}

GetEnumDescription()

Для получения описания мы создадим в перечислении расширяющий метод.

При создании подобных методов необходимо учитывать несколько правил.

Правило 1: расширяющие методы определяются как статические, но вызываются с помощью синтаксиса метода экземпляра. В примере ниже мы создаем статический метод GetEnumDescription().

Листинг 2. Пустое тело метода GetEnumDescription:

public static string GetEnumDescription() 
{
}

Правило 2: первый параметр указывает тип, для которого создается расширяющий метод. В данном примере это будет Enum. Этот параметр всегда обозначается модификатором this.

Листинг 3. GetEnumDescription() с параметром this:

public static string GetEnumDescription(this Enum enumValue) 
{
}

Правило 3: для доступа к расширяющим методам необходимо явно импортировать пространство имен. Для обращения к GetEnumDescription() нужно добавить в вызывающий класс пространство имен ExtensionMethod.

Листинг 3.1. GetEnumDescription() с пространством имен:

namespace ExtensionMethod 
{
public static class Extension
{
public static string GetEnumDescription(this Enum enumValue)
{
}
}
}

Правило 4: переопределение строго запрещается. Вы можете использовать расширяющие методы для расширения поведения класса или интерфейса, но не переопределения.

Теперь, разобравшись с правилами, пора добавить в это пустое тело логику. В листинге ниже в строке 3 я просто получаю fieldInfo типа enum, а в следующей ищу кастомный атрибут с типом DescriptionAttribute, и если тип совпадает, то просто возвращаю описание. В противном случае выбрасываю исключение.

Листинг 4. GetEnumDescription():

public static string GetEnumDescription(this Enum enumValue)
{
var field = enumValue.GetType().GetField(enumValue.ToString());
if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
{
return attribute.Description;
}
throw new ArgumentException("Item not found.", nameof(enumValue));
}

Все настроено, настал черед магии. Ориентируясь на диаграмму, мы вызовем расширяющий метод в классе Program. В листинге 5 мы создаем переменную с типом перечисления FlagshipSmartphone, имеющую значение Samsung. Затем в строке 4 вызываем расширяющий метод с помощью оператора ..

Листинг 5. Вызов расширяющего метода:

using ExtensionMethod;

FlagshipSmartphone Samsung = FlagshipSmartphone.Samsung;
string Description = Samsung.GetEnumDescription();
Console.WriteLine(Description);

Запустите приложение и увидите вывод, показанный на рис. 2:

Рис. 2: вывод GetEnumDescription()

GetEnumValueByDescription()

Теперь, когда мы познакомились со всеми элементами, пора переходить к коду. В нем мы повторно используем GetEnumDescription(), чтобы избежать переписывания одной и той же логики.

Листинг 6. GetEnumValueByDescription():

public static T GetEnumValueByDescription<T>(this string description) where T : Enum
{
foreach (Enum enumItem in Enum.GetValues(typeof(T)))
{
if (enumItem.GetEnumDescription() == description)
{
return (T)enumItem;
}
}
throw new ArgumentException("Not found.", nameof(description));
}
  • У вас может возникнуть вопрос, а что мы вообще делаем в листинге 6?
  • Сначала мы используем первый параметр string в качестве this.
  • Затем мы возвращаем параметр общего типа <T>, чтобы сделать этот метод переиспользуемым по всем типам перечислений.
  • Согласно логике, мы перебираем значения перечислений в поиске соответствующих описаний. Как только описание совпадает, мы просто возвращаем это значение перечисления. Если же совпадений не обнаруживается, выбрасывается исключение. Этот момент вы можете обработать по-своему.

Идем дальше и вызываем этот метод в классе Program. Как видите на строке 8, для вызова расширяющего метода мы используем в строковой переменной iphone оператор ..

Листинг 7. Вызов GetEnumValueByDescription() на строке 8:

using ExtensionMethod;

FlagshipSmartphone Samsung = FlagshipSmartphone.Samsung;
string Description = Samsung.GetEnumDescription();
Console.WriteLine(Description);

string iphone = "iPhone 13 Pro Max";
FlagshipSmartphone flagship = iphone.GetEnumValueByDescription<FlagshipSmartphone>();
Console.WriteLine(flagship);

Выполните этот фрагмент, и вы должны увидеть вывод как на рис. 3.

Рис. 3: вывод GetEnumValueByDescription()

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

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

Листинг 8. Вызов расширяющего метода с помощью статического класса:

string iphone = "iPhone 13 Pro Max";
FlagshipSmartphone flagship = Extension.GetEnumValueByDescription<FlagshipSmartphone>(iphone);

В С# присутствует ряд предопределенных расширяющих методов.

Если рассмотреть следующий пример, то в нем у нас есть массив int, для которого мы используем FirstOrDefault().

Но ведь в этом массиве нет методов с именем FirstOrDefault(). Дело в том, что FirstOrDefault() —  это расширяющий метод, который можно использовать любой коллекцией, реализующей интерфейс IEnumerable.

В нашем случае массив этому требованию соответствует, что можно видеть на рис. 4.

Листинг 9. Использование расширяющего метода FirstOrDefault():

int[] inputArray = { -1, 5, 10, 25, 9, 48, 67}; 
int numberTen = inputArray.FirstOrDefault(x=> x == 10);
Рис. 4: class Array

Как мы узнали в листинге 8, расширяющий метод можно вызывать непосредственно с помощью статического класса. Значит, можно переписать код листинга 9, как показано ниже.

Примечание: первый параметр  —  это this типа коллекции. То есть в качестве первого параметра в метод FirstOrDefault() нужно будет передать массив int.

Листинг 10. Вызов расширяющего метода с помощью статического класса:

int[] inputArray = { -1, 5, 10, 25, 9, 48, 67};
int numberTen = Enumerable.FirstOrDefault(inputArray, x => x == 10);

Но всегда предпочтительнее использовать расширяющие методы, ведь именно для этого они и предназначены.

Эта тема более обширно раскрывает возможности расширения классов дополнительным поведением без их изменения.

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

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Rikam Palkar: Creating Extension Methods in C#

Предыдущая статьяМеняем Async/Await на Promises.allSettled() для ускорения API-вызовов в Node.JS
Следующая статьяДля подготовки к собеседованию: 10 задач по промисам JavaScript