Как стать автором
Обновить

Методы расширения в C#

Время на прочтение5 мин
Количество просмотров13K

Как их использовала Microsoft

Методы расширения - достаточно удобный механизм, который упрощает понимание кода в разы. Приведу в пример Microsoft, которая использовала данный механизм для создания LINQ.

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

var array = new[]
{
  "Array", "Adapter", "Class", "Interface"  
}; // Объявляем массив со строками

// Используем статичный метод Where в статичном классе Enumerable
var startsWithA = Enumerable.Where(array, s => s.StartsWith('A'));

// Выводим все результат
foreach (var s in startsWithA)
{
    Console.WriteLine(s);
}

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

var array = new[]
{
  "Array", "Adapter", "Class", "Interface"  
}; // Объявляем массив со строками

// Используем статичный метод расширения Where
var startsWithA = array.Where(s => s.StartsWith('A'));

// Выводим результат
foreach (var s in startsWithA)
{
    Console.WriteLine(s);
}

Строчка с отбором элементов коллекции стала опрятней. Давайте посмотрим на определение метода Where

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
                                                  Func<TSource, bool> predicate)

Ключевое слово this, стоящее перед первым аргументом, вершит всю магию. И если посмотреть на тип первого аргумента, то станет понятно, почему мы можем использовать данный метод для любой коллекции, которая наследует IEnumerable<T>

Создаем метод расширения для выведения членов массива в консоль

Теперь мы можем тоже использовать этот механизм для увеличения читаемости нашего кода

Задача: создать инструмент для вывода всех элементов массива в консоль

Создадим статичный класс CollectionWriter, в котором будет определен метод WriteToConsole, который как раз и выводит всю коллекцию

using System.Collections;

public static class CollectionWriter
{
    public static void WriteToConsole(IEnumerable collection)
    {
        foreach (var member in collection)
        {
            Console.WriteLine(member);
        }
    }
}

Сейчас этот метод выполняет свою задачу, для которой он был создан. Использовать его можно так

var array = new[] { "Member1", "Member2", "Member3" };

CollectionWriter.WriteToConsole(array);

Поставим ключевое слово this перед первым аргументом, создав тем самым для компилятора пометку, что данный метод является "расширяющим" (позже посмотрим какую именно метку ставит после нас компилятор для увеличения производительности). Объявление метода теперь имеет следующий вид

public static void WriteToConsole(this IEnumerable collection)

Теперь мы можем вызывать метод уже следующим образом

var array = new[] { "Member1", "Member2", "Member3" };

array.WriteToConsole();

По сути мы можем дословно перевести строчку 3 на человеческий язык.

array.WriteToConsole() = массив.ВывестиВКонсоль()

Нужно быть аккуратным в использовании этого механизма. Если в следующем обновлении библиотек Microsoft добавит, например, в класс List<T> наш метод WriteToConsole, то компилятор будет ставить в приоритет экземплярный метод, и программа будет вести себя как-нибудь по-другому.

Как это работает "под капотом"

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

Когда компилятор встречает array.WriteToConsole(), он в первую очередь начинает проверять на соответствие методы, которые находятся в текущем классе и его родителях. Если он не нашел подходящий, начинается уже поиск методов расширения по всех статических классах. Как только он нашел подходящий метод с пометкой this у первого аргумента, он генерирует IL код для него, помечая его и класс, в котором он определен, атрибутом ExtensionAttribute. Для нашего класса IL код выглядит так:

.class public abstract sealed auto ansi beforefieldinit
  CollectionWriter
    extends [System.Runtime]System.Object
{
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
    = (01 00 00 00 )

  .method public hidebysig static void
    WriteToConsole(
      class [System.Runtime]System.Collections.IEnumerable collection
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 01 00 00 ) // .....
      // unsigned int8(1) // 0x01
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 1
    .locals init (
      [0] class [System.Runtime]System.Collections.IEnumerator V_0,
      [1] object member,
      [2] class [System.Runtime]System.IDisposable V_2
    )

    // [10 5 - 10 6]
    IL_0000: nop

    // [11 9 - 11 16]
    IL_0001: nop

    // [11 32 - 11 42]
    IL_0002: ldarg.0      // collection
    IL_0003: callvirt     instance class [System.Runtime]System.Collections.IEnumerator [System.Runtime]System.Collections.IEnumerable::GetEnumerator()
    IL_0008: stloc.0      // V_0
    .try
    {

      IL_0009: br.s         IL_001b
      // start of loop, entry point: IL_001b

        // [11 18 - 11 28]
        IL_000b: ldloc.0      // V_0
        IL_000c: callvirt     instance object [System.Runtime]System.Collections.IEnumerator::get_Current()
        IL_0011: stloc.1      // member

        // [12 9 - 12 10]
        IL_0012: nop

        // [13 13 - 13 39]
        IL_0013: ldloc.1      // member
        IL_0014: call         void [System.Console]System.Console::WriteLine(object)
        IL_0019: nop

        // [14 9 - 14 10]
        IL_001a: nop

        // [11 29 - 11 31]
        IL_001b: ldloc.0      // V_0
        IL_001c: callvirt     instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
        IL_0021: brtrue.s     IL_000b
      // end of loop
      IL_0023: leave.s      IL_0037
    } // end of .try
    finally
    {

      IL_0025: ldloc.0      // V_0
      IL_0026: isinst       [System.Runtime]System.IDisposable
      IL_002b: stloc.2      // V_2
      IL_002c: ldloc.2      // V_2
      IL_002d: brfalse.s    IL_0036
      IL_002f: ldloc.2      // V_2
      IL_0030: callvirt     instance void [System.Runtime]System.IDisposable::Dispose()
      IL_0035: nop

      IL_0036: endfinally
    } // end of finally

    // [15 5 - 15 6]
    IL_0037: ret

  } // end of method CollectionWriter::WriteToConsole
} // end of class CollectionWriter

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

Пометка класса

.class public abstract sealed auto ansi beforefieldinit
  CollectionWriter
    extends [System.Runtime]System.Object
{
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
    = (01 00 00 00 ) // АТРИБУТ

Пометка метода

.method public hidebysig static void
    WriteToConsole(
      class [System.Runtime]System.Collections.IEnumerable collection
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 01 00 00 ) // .....
      // unsigned int8(1) // 0x01
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
      = (01 00 00 00 ) // АТРИБУТ

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

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

// [5 1 - 5 24]
IL_001f: ldloc.0      // 'array'
IL_0020: call         void CollectionWriter::WriteToConsole(class [System.Runtime]System.Collections.IEnumerable)
IL_0025: nop
IL_0026: ret

Из этого можно сделать вывод: если мы используем объект со значением null, то метод расширения выполнится без вызова исключения NullReferenceException (в отличие от экземлярного метода).

Заключение

Методы расширения - удобный механизм, который поможет увеличить читабельность Вашего кода. Они крайне полезны, когда нужно добавить метод к уже реализованному классу или интерфейсу (как, например, делала Microsoft в LINQ или мы с классом IEnumerable).

Теги:
Хабы:
Всего голосов 14: ↑10 и ↓4+6
Комментарии21

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань