Соединение коллекций


Соединение в 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

Google
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 }