Как их использовала 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).