Когда знакомишься с какой-то новой для себя темой по примерам, то достаточно быстро привыкаешь использовать новые «фичи» на автомате, особенно не вникая в то, какие ещё могут быть варианты работы. Например, при работе с LINQ активно используются лямбда-выражения, потому что это удобный и лаконичный способ работы с анонимными методами. Вместе с этим, в LINQ используются делегаты, которые мы можем, при необходимости, использовать как обычные методы. Сегодня рассмотрим как использовать делегаты в LINQ и как понять «что от нас хотят разработчики LINQ», читая исходный код LINQ
Стандартные делегаты в C#
В C# предусмотрен ряд стандартных делегатов, которые мы можем использовать при разработке своих приложений вместо того, чтобы создавать эти делегаты самостоятельно. Самые часто используемые стандартные делегаты, с которыми мы можем сталкиваться — это Action
, Predicate
и Func
с различным количеством параметров. В LINQ наиболее часто используются делегаты Func
. Посмотрим, что из себя представляет этот делегат.
Func<TResult>
— этот делегат инкапсулирует метод, который не имеет параметров и возвращает значение типа, указанного в параметре TResult
. Под TResult
при этом может скрываться любой тип данных. Например, если нам встречается в какой-то библиотеке информация, что метод принимает в качестве аргумента вот такой делегат:
Func<int>
то это значит, что мы должны написать в своем коде метод наподобие такого:
public int MyDelegate() { //какие-то действия return 1; //вернуть должны int }
то есть сигнатура нашего метода должна в точности соответствовать делегату, а уж что будет внутри метода — решать нам. Главное, чтобы метод возвращал данные определенного типа, например, int
и не принимал ничего в качестве входных параметров.
Для делегата Func
имеется ряд перегрузок в которых каждая очередная версия делегата принимает различное количество параметров (до шестнадцати). Посмотрим на следующую версию делегата:
Func<T1,T2,T3,TResult>
— инкапсулирует метод, который имеет три параметра и возвращает значение типа, указанного параметром TResult
, имеет следующее описание:
public delegate TResult Func<in T1,in T2,in T3,out TResult>(T1 arg1, T2 arg2, T3 arg3);
опять же, мы должны или написать метод, соответствующий делегату, или использовать в параметрах метода, требующего этот делегат лямбда-выражение.
То есть, из всего сказанного мы должны для себя уяснить следующее: в делегатах вида Func<T1, T2....T16, TResult>
параметр TResult
— обозначает то, результат какого типа должен вернуть делегат, все остальные параметры (T1...T16
), указанные в описание — это параметры метода, которые он принимает.
Теперь посмотрим, как эти делегаты используются нами в LINQ.
Использование делегатов в LINQ
Рассмотрим работу делегатов в LINQ на примере следующей последовательности объектов:
public class Person { public int Age { get; set; } public string LastName { get; set; } public string FirstName { get; set; } } Person[] people = new Person[] { new Person(){ FirstName="Василий", LastName="Пупкин", Age=15}, new Person(){ FirstName="Иван", LastName="Иванов", Age=18}, new Person(){ FirstName="Петр", LastName="Петров", Age=25}, new Person(){ FirstName="Семен", LastName="Семенов",Age=55} };
Допустим, мы хотим, используя метод LINQ Select
получить последовательность объектов, которые будут содержать вместо FirstName
и LastName
поле FullName
, сочетающее в себе имя и фамилию. В привычной для нас форме (с использованием лямбда-выражений) мы напишем следующий код:
var result = people.Select(f=>new { FullName = $"{f.FirstName} {f.LastName}", f.Age});
на выходе получим последовательность объектов, которые затем можем перебрать в цикле, например, так:
foreach (var element in result) { Console.WriteLine(element.FullName); }
Теперь посмотрим на описание метода Select
. Этот метод имеет две версии:
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> selector)
Здесь мы видим:
- метод вернет нам некую последовательность элементов (
IEnumerable
) и, при этом, элементы будут иметь типTResult
— тот тип, который мы хотим получить. Какой это тип — решать нам - первый параметр метода —
this IEnumerable<TSource>
— сама последовательность к которой применяется метод, указывать её при вызовеSelect
не требуется. - второй параметр метода -делегат вида
Func<TSource, TResult>
или жеFunc<TSource, int, TResult>
, то есть делегат принимает либо один параметр (TSource
), либо два (TSource
иint
) и должен вернутьTResult
(тип, который мы должны сами определить)
Попробуем вместо лямбда-выражения передать теперь в Select
метод делегата. Начнем с первого варианта (метод с одним параметром). Вот как может выглядеть наш метод:
static dynamic Transform(Person person) { return new { FullName = $"{person.FirstName} {person.LastName}", person.Age }; }
Так как мы используем Select
для массива Person[]
, то и входной параметр метода у нас Person
, то есть, фактически, TSource == Person
. Возвращается из метода тип dynamic
, чтобы лишний раз не использовать вручную рефлексию и не определять какие свойства имеет наш возвращаемый из метода новый объект. Использовать этот метод в Select может так:
var result = people.Select(Transform);
результат работы приложения будет абсолютно тот же, что и при использовании привычного нам лямбда-выражения.
Второй вариант метода Select принимает два параметра — некий объект TSource и число int. При этом, число — это порядковый номер элемента в исходной последовательности. Для примера, пусть наш метод будет формировать объект с новым свойством Id, которое будет формироваться на основании порядкового номера элемента в исходной последовательности.
static dynamic Transform(Person person, int index) { return new {Id = ++index, FullName = $"{person.FirstName} {person.LastName}", person.Age }; }
Используем этот метод в Select
и посмотрим на результат:
var result = people.Select(Transform); foreach (var element in result) { Console.WriteLine($"{element.Id} {element.FullName}"); }
2 Иван Иванов
3 Петр Петров
4 Семен Семенов
С использованием лямбда-выражения вызов метода мог бы выглядеть так:
var result = people.Select((person, index)=> new { Id = ++index, FullName = $"{person.FirstName} {person.LastName}", person.Age });
Аналогичным образом можно продемонстрировать использование делегатов и в более сложных методах LINQ. Например, посмотрим на метод Join
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector)
здесь мы уже видим следующие параметры:
this IEnumerable<TOuter> outer
— первая последовательность (та к которой применяется метод). Например, наш массивPerson[]
IEnumerable<TInner> inner
— вторая последовательность (та, с которой будет соединяться первая последовательность) — её необходимо указать при вызове методаFunc<TOuter, TKey> outerKeySelector
— метод, на основании которого будет происходить определение ключа для первой последовательность (TKey
— тип ключа)Func<TInner, TKey> innerKeySelector
— метод, на основании которого будет происходить определение ключа для второй последовательностиFunc<TOuter, TInner, TResult> resultSelector
— метод на основании которого будут формироваться элементы результирующей последовательности.
Например, пусть каждому Id
человека из последовательности, которую мы получили в Select
будет соответствовать свой адрес проживания. Вторая последовательность будет выглядеть следующим образом:
public class Address { public int Id { get; set; } public string Street { get; set; } } Address[] addresses = new Address[] { new Address(){ Id = 1, Street = "Кемеровская, 23"}, new Address(){ Id = 2, Street = "Челябинская, 10"}, new Address(){ Id = 3, Street = "Крыловская, 25"}, new Address(){ Id = 4, Street = "Ивановская, 18"}, };
Вначале рассмотрим привычный вариант использования Join
— с лямбда-выражением:
var result = people.Select(Transform); //получили набор объектов с Id var withAddr = result.Join(addresses, //вторая последовательность people => people.Id, //выбираем ключ для первой последовательности address => address._Id, //выбираем ключ для второй последовательности (people, address) => new {people.Id, people.FullName, address.Street });//создаем новый объект foreach (var element in withAddr) { Console.WriteLine($"{element.Id} {element.FullName} {element.Street}"); }
результат будет таким:
2 Иван Иванов Челябинская, 10
3 Петр Петров Крыловская, 25
4 Семен Семенов Ивановская, 18
Теперь представим точно такую же работу метода, но уже с использованием обычных методов в качестве параметров в Join
.
Пишем метод выбора ключа для первой последовательности. Этот метод должен принимать в параметрах тот объект, который мы создали в предыдущем примере (с типом dynamic
) и возвращать ключ — это у нас Id
пользователя:
static int OuterKeySelector(dynamic people) { return people.Id; }
Аналогичный метод для выбора ключа второй последовательности
static int InnerKeySelector(Address address) { return address.Id; }
И метод получения объекта для результирующей последовательности:
static dynamic ResultSelector(dynamic people, Address address) { return new { people.Id, people.FullName, address.Street }; }
Теперь изменим код вызова Join
для использования этих методов вместо лямбд:
var withAddr = result.Join(addresses, //вторая последовательность OuterKeySelector, //выбираем ключ для первой последовательности InnerKeySelector, //выбираем ключ для второй последовательности ResultSelector);//создаем новый объект
Результат будет аналогичным.
Итого
При работе с методами LINQ мы можем передавать в качестве параметров методов как лямбда-выражения, так и полноценные методы. В большинстве случаев лямбды позволяют сделать код более лаконичным и понятным, однако использование полноценных методов может быть полезным в тех случаях, когда нам требуется выполнение каких-либо операций над объектами исходной последовательности и есть вероятность того, что лямбда-выражение может «разрастись». Думаю, что в этом случае, код с полноценными методами может оказаться более понятным.