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