Соединение коллекций
Соединение в LINQ используется для объединения двух разнотипных наборов в один. Для соединения используется оператор join или метод Join(). Как правило, данная операция применяется к двум наборам, которые имеют один общий критерий.
Оператор join
Оператор join имеет следующий формальный синтаксис:
from объект1 in набор1
join объект2 in набор2 on объект2.свойство2 equals объект1.свойство1
После оператора join идет выборка объектов из второй коллекции. После оператора on указывается критерий соединения - свойство объекта из второй выборки, а после оператора equals - свойство объекта из первой выборки, которому должно быть равно свойство объекта из второй выборки. Если эти свойства равны, то оба объекта попадают в финальный результат.
Например, у нас есть два класса:
record class Person(string Name, string Company);
record class Company(string Title, string Language);
Класс Person представляет пользователя и хранит два свойства: Name (имя) и Company (компания пользователя). Класс Company представляет компанию и хранит два свойства: Title (название компании) и Language (основной язык программирования в компании)
Объекты обоих классов будет иметь один общий критерий - название компании. Соединим по этому критерию два набора этих классов:
Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
};
Company[] companies =
{
new Company("Microsoft", "C#"),
new Company("Google", "Go"),
new Company("Oracle", "Java")
};
var employees = from p in people
join c in companies on p.Company equals c.Title
select new { Name = p.Name, Company = c.Title, Language = c.Language };
foreach (var emp in employees)
Console.WriteLine($"{emp.Name} - {emp.Company} ({emp.Language})");
record class Person(string Name, string Company);
record class Company(string Title, string Language);
С помощью выражения
join c in companies on p.Company equals c.Title
объект p из списка people (то есть объект Person) соединяется с объектом c из списка companies (то есть с объектом Company), если значение свойства p.Company совпадает со значением свойства c.Title. Результатом соединения будет объект анонимного типа, который будет содержать три свойства. В итоге мы получим следующий вывод:
Tom - Microsoft (C#)
Sam - Google (Go)
Mike - Microsoft (C#)
Обратите внимание, что в массиве people есть объект new Person("Bob", "JetBrains"), но в массиве компаний компании с именем "JetBrains" нет, соответственно он не попал с результат. Аналогично в списке people нет объектов Person, которые бы соотствовали компании new Company("Oracle", "Java").
Метод Join
В качестве альтернативы можно было бы использовать метод Join():
Join(IEnumerable<TInner> inner,
Func<TOuter,TKey> outerKeySelector,
Func<TInner,TKey> innerKeySelector,
Func<TOuter,TInner,TResult> resultSelector);
Метод Join() принимает четыре параметра:
второй список, который соединяем с текущим
делегат, который определяет свойство объекта из текущего списка, по которому идет соединение
делегат, который определяет свойство объекта из второго списка, по которому идет соединение
делегат, который определяет новый объект в результате соединения
Перепишим предыдущий пример с использованием метода Join:
Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
};
Company[] companies =
{
new Company("Microsoft", "C#"),
new Company("Google", "Go"),
new Company("Oracle", "Java")
};
var employees = people.Join(companies, // второй набор
p => p.Company, // свойство-селектор объекта из первого набора
c => c.Title, // свойство-селектор объекта из второго набора
(p, c) => new { Name = p.Name, Company = c.Title, Language = c.Language }); // результат
foreach (var emp in employees)
Console.WriteLine($"{emp.Name} - {emp.Company} ({emp.Language})");
record class Person(string Name, string Company);
record class Company(string Title, string Language);
GroupJoin
Метод GroupJoin() кроме соединения последовательностей также выполняет и группировку.
GroupJoin(IEnumerable<TInner> inner,
Func<TOuter,TKey> outerKeySelector,
Func<TInner,TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>,TResult> resultSelector);
Метод GroupJoin() принимает четыре параметра:
второй список, который соединяем с текущим
делегат, который определяет свойство объекта из текущей коллекции, по которому идет соединение и по которому будет идти группировка
делегат, который определяет свойство объекта из второй коллекции, по которому идет соединение
делегат, который определяет новый объект в результате соединения. Этот делегат получает группу - объект текущей коллекции, по которому шла группировка, и набор объектов из второй коллекции, которые сооставляют группу
Например, возьмем выше определенные массивы people и companies и сгуппируем всех пользователей по компаниям:
Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
};
Company[] companies =
{
new Company("Microsoft", "C#"),
new Company("Google", "Go"),
new Company("Oracle", "Java")
};
var personnel = companies.GroupJoin(people, // второй набор
c => c.Title, // свойство-селектор объекта из первого набора
p => p.Company, // свойство-селектор объекта из второго набора
(c, employees) => new // результат
{
Title = c.Title,
Employees = employees
});
foreach (var company in personnel)
{
Console.WriteLine(company.Title);
foreach(var emp in company.Employees)
{
Console.WriteLine(emp.Name);
}
Console.WriteLine();
}
record class Person(string Name, string Company);
record class Company(string Title, string Language);
Результатом выполнения программы будет следующий вывод:
Microsoft
Tom
Mike
Sam
Oracle
Метод GroupJoin, также как и метод Join, принимает все те же параметры. Только теперь в последний параметр - делегат передаются объект компании и набор пользователей этой компании.
Обратите внимание, что для компании "Oracle" в массиве people нет пользователей, хотя для нее также создается группа.
Аналогичного результата можно добитьс и с помощью оператора join:
var personnel = from c in companies
join p in people on c.Title equals p.Company into g
select new // результат
{
Title = c.Title,
Employees = g
};
Метод Zip
Метод Zip() последовательно объединяет соответствующие элементы текущей последовательности со второй последовательностью, которая передается в метод в качестве параметра. То есть первый элемент из первой последовательности объединяется с первым элементом из второй последовательности, второй элемент из первой последовательности соединяется со вторым элементом из второй последовательности и так далее. Результатом метода является коллекция кортежей, где каждый кортеж хранит пару соответствующих элементов из обоих последовательностей:
var courses = new List<Course> { new Course("C#"), new Course("Java") };
var students = new List<Student> { new Student("Tom"), new Student("Bob") };
var enrollments = courses.Zip(students);
foreach (var enrollment in enrollments)
Console.WriteLine($"{enrollment.First} - {enrollment.Second}");
record class Course(string Title); // учебный курс
record class Student(string Name); // студент
Здесь метод Zip объединяет соответствующие элементы из списков courses и students. В результате создается новая коллекция, которая хранит набор кортежей. Каждый кортеж в ней имеет два элемента. Первый элемент из свойства First представляет объект текущей коллекции (в данном случае объект Course), а второй элемент (в свойстве Second) хранит объект второй последовательности (в данном случае объект Student). Консольный вывод:
Course { Title = C# } - Student { Name = Tom }
Course { Title = Java } - Student { Name = Bob }