Когда знакомишься с какой-то новой для себя темой по примерам, то достаточно быстро привыкаешь использовать новые «фичи» на автомате, особенно не вникая в то, какие ещё могут быть варианты работы. Например, при работе с 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 мы можем передавать в качестве параметров методов как лямбда-выражения, так и полноценные методы. В большинстве случаев лямбды позволяют сделать код более лаконичным и понятным, однако использование полноценных методов может быть полезным в тех случаях, когда нам требуется выполнение каких-либо операций над объектами исходной последовательности и есть вероятность того, что лямбда-выражение может «разрастись». Думаю, что в этом случае, код с полноценными методами может оказаться более понятным.