Проекция данных
Проекция позволяет преобразовать объект одного типа в объект другого типа. Для проекции используется оператор 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>