Введение
Согласно MSDN, расширяющие методы позволяют “добавлять” методы в существующие типы без создания производного типа, рекомпиляции и внесения прочих изменений. Расширяющие методы статичны, но вызываются так, будто являются методами экземпляра в расширенном типе.
Звучит это все здорово, но давайте разберемся подробнее.
Расширяющие методы представляют прекрасный пример принципа открытости/закрытости, который гласит: “открыт для расширения, закрыт для изменения”. Программные сущности должны быть открыты для расширения, но закрыты для изменения. Проще говоря, сущность должна легко расширяться без внутреннего изменения.
То есть теперь у вас есть возможность явно добавить новый метод в существующий класс String
, и если вы хотите проверить, является ли строка палиндромом, то можете просто создать расширяющий метод.
Но как это происходит?
Мы не будем использовать очередной пример с палиндромами. Вместо этого мы реализуем что-нибудь полезное. Напишем расширяющий метод для получения описания enum
по его значению.
Реализация
Начнем с основного: “Можно ли внести изменения в класс String
?».
Конечно, нет. У вас нет возможности вносить какие-либо изменения в системные классы. Тут вы можете поспорить, что можно наследовать класс и расширить его дополнительной функциональностью, переопределив существующие методы. Однако эта опция также недоступна, поскольку это запечатанный класс, а значит для наследования он закрыт.
Взгляните на рис. 1 — это общий план наших действий.
Как видно на изображении выше, здесь у нас два расширяющих метода.
- Первый,
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")]
}
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:
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.
Вот как можно расширять поведение существующих классов без их изменения. Для этого существуют и другие паттерны проектирования, но в данном случае мы выбираем короткий путь.
Есть и еще один интересный момент. Поскольку это пользовательский статический класс, то вы также можете вызывать этот метод непосредственно с помощью имени класса.
Листинг 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);
Как мы узнали в листинге 8, расширяющий метод можно вызывать непосредственно с помощью статического класса. Значит, можно переписать код листинга 9, как показано ниже.
Примечание: первый параметр — это this
типа коллекции. То есть в качестве первого параметра в метод FirstOrDefault()
нужно будет передать массив int
.
Листинг 10. Вызов расширяющего метода с помощью статического класса:
int[] inputArray = { -1, 5, 10, 25, 9, 48, 67};
int numberTen = Enumerable.FirstOrDefault(inputArray, x => x == 10);
Но всегда предпочтительнее использовать расширяющие методы, ведь именно для этого они и предназначены.
Эта тема более обширно раскрывает возможности расширения классов дополнительным поведением без их изменения.
Кроме того, она позволяет лучше понять принцип работы статических классов. Можете заглянуть в системные классы в поиске расширяющих методов и наверняка будете удивлены, как много вы их уже использовали.
Читайте также:
- String и string в С#: больше, чем просто стиль?
- 8 рекомендаций по написанию читаемого кода на C# с помощью .NET 6
- Что вы знаете о C#
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Rikam Palkar: Creating Extension Methods in C#