Делегаты в LINQ

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

Здесь мы видим:

  1. метод вернет нам некую последовательность элементов (IEnumerable) и, при этом, элементы будут иметь тип TResult — тот тип, который мы хотим получить. Какой это тип — решать нам
  2. первый параметр метода — this IEnumerable<TSource> — сама последовательность к которой применяется метод, указывать её при вызове Select не требуется.
  3. второй параметр метода -делегат вида 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}");
}
1 Василий Пупкин
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}");
}

результат будет таким:

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии