Проекция данных


Проекция позволяет преобразовать объект одного типа в объект другого типа. Для проекции используется оператор select. Допустим, у нас есть набор объектов следующего класса, представляющего пользователя:

record class Person(string Name, int Age);
Но, допустим, нам нужен не весь объект, а только его свойство Name:

var people = new List<Person>
{
new Person ("Tom", 23),
new Person ("Bob", 27),
new Person ("Sam", 29),
new Person ("Alice", 24)
};

var names = from p in people select p.Name;

foreach (string n in names)
Console.WriteLine(n);
Результат выражения LINQ будет представлять набор строк, поскольку выражение select p.Name выбирает в результирующую выборку только значения свойства Name.

Tom
Bob
Sam
Alice
В качестве альтернативы мы могли бы использовать метод расширения Select():

Select(Func<TSource,TResult> selector)
Этот метод принимает функцию преобразования в виде делегата Func<TSource,TResult>. Функция преобразования получает каждый объект выборки типа TSource и с его помощью создает объект TResult. Метод Select возвращает коллекцию преобразованных объектов.

Перепишем предыдущий пример с применением метода Select:

var people = new List<Person>
{
new Person ("Tom", 23),
new Person ("Bob", 27),
new Person ("Sam", 29),
new Person ("Alice", 24)
};
var names = people.Select(u => u.Name);

foreach (string n in names)
Console.WriteLine(n);
Аналогично можно создать объекты другого типа, в том числе анонимного:

var people = new List<Person>
{
new Person ("Tom", 23),
new Person ("Bob", 27)
};

var personel = from p in people
select new
{
FirstName = p.Name,
Year = DateTime.Now.Year - p.Age
};

foreach (var p in personel)
Console.WriteLine($"{p.FirstName} - {p.Year}");


record class Person(string Name, int Age);
Здесь оператор select создает объект анонимного типа, используя текущий объект Person. И теперь результат будет содержать набор объектов данного анонимного типа, в котором определены два свойства: FirstName и Year (год рождения). Консольный вывод программы:

Tom - 1999
Bob - 1995
В качестве альтернативы мы могли бы использовать метод расширения Select():

// проекция на объекты анонимного типа
var personel = people.Select(p => new
{
FirstName = p.Name,
Year = DateTime.Now.Year - p.Age
});
Переменые в запросах и оператор let
Иногда возникает необходимость произвести в запросах LINQ какие-то дополнительные промежуточные вычисления. Для этих целей мы можем задать в запросах свои переменные с помощью оператора let:

var people = new List<Person>
{
new Person ("Tom", 23),
new Person ("Bob", 27)
};

var personnel = from p in people
let name = $"Mr. {p.Name}"
let year = DateTime.Now.Year - p.Age
select new
{
Name = name,
Year = year
};

foreach (var p in personnel)
Console.WriteLine($"{p.Name} - {p.Year}");

record class Person(string Name, int Age);
В данном случае создаются две переменных. Переменная name, значение которой равно $"Mr. {p.Name}".

Возможность определения переменных наверное одно из главных преимуществ операторов LINQ по сравнению с методами расширения.

Выборка из нескольких источников
В LINQ можно выбирать объекты не только из одного, но и из большего количества источников. Например, возьмем классы:

record class Course(string Title); // учебный курс
record class Student(string Name); // студент
Класс Course представляет учебный курс и хранит его название. Класс Student представляет студента и хранит его имя.

Допустим, нам надо из списка курсов и списка студентов получить набор пар студент-курс (условно говоря сущность, которая представляет учебу студента на данном курсе):

var courses = new List<Course> { new Course("C#"), new Course("Java") };
var students = new List<Student> { new Student("Tom"), new Student("Bob") };

var enrollments = from course in courses // выбираем по одному курсу
from student in students // выбираем по одному студенту
select new { Student = student.Name, Course = course.Title}; // соединяем каждого студента с каждым курсом

foreach (var enrollment in enrollments)
Console.WriteLine($"{enrollment.Student} - {enrollment.Course}");

record class Course(string Title); // учебный курс
record class Student(string Name); // студент
Консольный вывод:

Tom - C#
Bob - C#
Tom - Java
Bob - Java
Таким образом, при выборке из двух источников каждый элемент из первого источника будет сопоставляться с каждым элементом из второго источника. То есть получиться 4 пары.

SelectMany и сведение объектов
Метод SelectMany позволяет свести набор коллекций в одну коллекцию. Он имеет ряд перегруженных версий. Возьмем одну из них:

SelectMany(Func<TSource, IEnumerable<TResult>> selector);
SelectMany(Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource,TCollection,TResult> resultSelector);
Первая версия метода принимает функцию преобразования в виде делегата Func<TSource,IEnumerable<TResult>>. Функция преобразования получает каждый объект выборки типа TSource и с его помощью создает набор объектов TResult. Метод SelectMany возвращает коллекцию преобразованных объектов.

Вторая версия принимает функцию преобразования в виде делегата Func<TSource,IEnumerable<TResult>>. Функция преобразования получает каждый объект выборки типа TSource и возвращает некоторую промежуточную коллекцию типа TCollection. Второй параметр - то же функция функция преобразования в виде делегата Func<TSource,TCollection,TResult>, которая получает два параметра - каждый элемент текущей выборки и каждый элемент промежуточной коллекции и на их основе создает некоторый объект типа TResult.

Рассмотрим следующий пример:

var companies = new List<Company>
{
new Company("Microsoft", new List<Person> {new Person("Tom"), new Person("Bob")}),
new Company("Google", new List<Person> {new Person("Sam"), new Person("Mike")}),
};
var employees = companies.SelectMany(c => c.Staff);

foreach (var emp in employees)
Console.WriteLine($"{emp.Name}");

record class Company(string Name, List<Person> Staff);
record class Person(string Name);
Здесь нам дан список компаний, в каждой компании имеет набор сотрудников в виде списка объектов Person. И на выходе мы получаем список сотрудников всех компаний, то есть по сути коллекцию объектов Person. Консольный вывод:

Tom
Bob
Sam
Mike
Аналогичный пример с помощью операторов LINQ:

var companies = new List<Company>
{
new Company("Microsoft", new List<Person> {new Person("Tom"), new Person("Bob")}),
new Company("Google", new List<Person> {new Person("Sam"), new Person("Mike")}),
};
var employees = from c in companies
from emp in c.Staff
select emp;

foreach (var emp in employees)
Console.WriteLine($"{emp.Name}");

record class Company(string Name, List<Person> Staff);
record class Person(string Name);
Теперь добавим к сотрудникам их компанию:

var companies = new List<Company>
{
new Company("Microsoft", new List<Person> {new Person("Tom"), new Person("Bob")}),
new Company("Google", new List<Person> {new Person("Sam"), new Person("Mike")}),
};

var employees = companies.SelectMany(c => c.Staff,
(c, emp)=> new { Name = emp.Name, Company = c.Name });

foreach (var emp in employees)
Console.WriteLine($"{emp.Name} - {emp.Company}");

record class Company(string Name, List<Person> Staff);
record class Person(string Name);
Здесь применяется другая версия метода SelectMany. Первый делегат в виде c => c.Staff создает промежуточную коллекцию - фактически просто возвращаем набор сотрудников каждой компании. Второй делегат - (c, emp)=> new { Name = emp.Name, Company = c.Name } получает каждую компанию и каждый элемент промежуточной коллекции - объект Person и на их основе создает анонимный объект с двумя свойствами Name и Company. Консольный вывод программы:

Tom - Microsoft
Bob - Microsoft
Sam - Google
Mike - Google
Аналогичный пример с помощью операторов запросов:

var companies = new List<company>
{
new Company("Microsoft", new List<Person> {new Person("Tom"), new Person("Bob")}),
new Company("Google", new List<Person> {new Person("Sam"), new Person("Mike")}),
};
var employees = from c in companies
from emp in c.Staff
select new { Name = emp.Name, Company = c.Name };

foreach (var emp in employees)
Console.WriteLine($"{emp.Name} - {emp.Company}");

record class Company(string Name, List<Person> Staff);
record class Person(string Name);
</company>