1. Вступ
Іноді нам потрібно працювати з двома різними колекціями, пов’язаними спільною ознакою. Наприклад, у нас є список замовлень і окрема колекція покупців. Як зрозуміти, яке замовлення належить якому покупцеві? Для цього треба, щоб обидві колекції були пов’язані спільним ідентифікатором — наприклад, userId.
У реляційних базах даних таке завдання розв’язує операція JOIN, яка об’єднує рядки з різних таблиць за збігом ключів. У LINQ є аналогічний оператор — він теж називається Join.
Уявіть дві таблиці: у першій — список читачів бібліотеки з їхніми ідентифікаторами, а у другій — список замовлень книжок, де в кожному замовленні вказано id читача. За допомогою join ми «склеюємо» ці таблиці за id і в підсумку отримуємо пари «читач + його замовлення».
До речі, якщо ви вважали, що join — це лише «для баз даних», ви чимало втрачали. У програмуванні об’єднувати колекції доводиться часто, особливо коли працюєте із зовнішніми системами або структурованими файлами (JSON, XML чи таблицями Excel з бухгалтерії).
2. Сигнатура методу Join і принцип роботи
Ось як виглядає метод Join:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // перша колекція (зовнішня)
IEnumerable<TInner> inner, // друга колекція (внутрішня)
Func<TOuter, TKey> outerKeySelector, // як отримати ключ із елемента зовнішньої колекції
Func<TInner, TKey> innerKeySelector, // як отримати ключ із внутрішньої колекції
Func<TOuter, TInner, TResult> resultSelector) // функція формування результату (новий елемент)
На перший погляд це може здатися громіздким, але зараз усе розкладемо по поличках.
Як це працює:
Для кожного елемента з першої колекції (outer) LINQ шукає відповідні (за ключем) елементи з другої (inner). Коли ключі збігаються, викликається resultSelector, і результат додається до фінальної колекції.
3. Приклад: Об’єднуємо покупців і їхні замовлення
Створимо пару класів і колекцій. Ця ідея — логічне продовження нашого навчального застосунку (нехай це буде невеликий магазин, який ми пишемо впродовж курсу).
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Product { get; set; } = "";
}
// Колекція покупців
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Василь" },
new Customer { Id = 2, Name = "Петро" },
new Customer { Id = 3, Name = "Марко" },
};
// Колекція замовлень
var orders = new List<Order>
{
new Order { Id = 101, CustomerId = 2, Product = "Книгу" },
new Order { Id = 102, CustomerId = 1, Product = "Ручку" },
new Order { Id = 103, CustomerId = 2, Product = "Зошит" },
new Order { Id = 104, CustomerId = 3, Product = "Гумку" },
};
Тепер ми хочемо отримати всіх покупців із їхніми замовленнями. Наприклад, вивести: «Петро замовив Книгу», «Петро замовив Зошит» тощо.
Застосовуємо Join
// Об’єднуємо покупців із замовленнями за customer.Id і order.CustomerId
var query = customers.Join(
orders,
customer => customer.Id, // як отримати ключ із покупця
order => order.CustomerId, // як отримати ключ із замовлення
(customer, order) => new // що робити з парами, що збіглися (створюємо новий об’єкт)
{
CustomerName = customer.Name,
Product = order.Product
}
);
// Виводимо результат
foreach (var item in query)
{
Console.WriteLine($"{item.CustomerName} замовив {item.Product}");
}
Що буде виведено:
Василь замовив Ручку
Петро замовив Книгу
Петро замовив Зошит
Марко замовив Гумку
Зверніть увагу: якщо в покупця кілька замовлень, він з’явиться у списку кілька разів — по одному на кожне замовлення. Саме так і має бути!
4. Таблиця: Порівняння Join і GroupBy + SelectMany
| Операція | Результат | Сценарій використання |
|---|---|---|
|
Плоский список пар | Класичний SQL JOIN. Кожен збіг — окремий рядок. |
|
Групи з підколекціями | Потрібно отримати «покупець + всі його замовлення» у вигляді структури «один — до багатьох». |
Новачки часто використовують Join там, де потрібна структура «покупець → список замовлень». Але Join працює поелементно і не групує дані. Для цього є GroupJoin — про нього в наступній лекції. Або ж використовуйте GroupBy.
5. Join у Query Syntax (SQL-стиль LINQ)
LINQ підтримує два синтаксиси: метод-ланцюжки (Method Syntax, наприклад Join(...)) і так званий SQL-стиль (Query Syntax), який візуально нагадує звичайні SQL-запити.
Для деяких розробників — особливо тих, хто раніше працював із базами даних — такий синтаксис часто здається наочнішим:
var query2 =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
Product = order.Product
};
foreach (var item in query2)
{
Console.WriteLine($"{item.CustomerName} замовив {item.Product}");
}
Зверніть увагу:
У Query Syntax використовується ключове слово equals — оператор == тут не працює!
Це поширений підступ на співбесідах, особливо для початківців 😉
6. Важливі деталі та підводні камені
Іноді виникає ситуація, коли не всі елементи з колекцій потрапляють у фінальний результат. Це пов’язано з тим, що метод Join реалізує так званий «inner join», тобто він об’єднує лише ті елементи, у яких ключі збігаються. Якщо в якогось покупця немає замовлень, його просто не буде у фінальному списку. Аналогічно, якщо є замовлення з CustomerId, якого немає серед покупців, таке замовлення теж не потрапить у результат.
Що робити, якщо потрібно отримати всіх покупців, навіть тих, у кого немає замовлень? Для таких випадків існує «лівий» join, який у LINQ реалізується за допомогою комбінації GroupJoin і SelectMany. Про це поговоримо в наступній лекції. У класичному ж Join обов’язково мають бути представлені обидві сторони — інакше збігу не буде, і елемент випаде з результату.
7. Робота з кількома ключами (складений ключ)
Іноді об’єднувати колекції треба не за одним, а за кількома полями. Наприклад: з’єднати товари та їхні продажі за «код товару + рік продажу».
У LINQ це вирішується за допомогою створення анонімних об’єктів як ключів:
var sales = ...; // продажі
var products = ...; // товари
var query = products.Join(
sales,
prod => new { prod.Code, prod.Year },
sale => new { sale.ProductCode, sale.Year },
(prod, sale) => new { prod.Name, sale.Amount }
);
Важливо, щоб типи та імена властивостей у цих анонімних об’єктах збігалися. Якщо вони різні, збігів не буде — навіть якщо значення по суті однакові.
8. Як виглядає об’єднана колекція — схема
Ось спрощена схема, що ілюструє роботу join:
Із схеми видно, що кожен покупець з’єднується з усіма своїми замовленнями — за умови збігу ключів.
9. Коротко про помилки та особливості
Одна з найчастіших помилок — переплутати порядок параметрів, особливо якщо типи ключів в обох колекціях однакові. При цьому компілятор або LINQ не повідомлять про помилку, але результат може бути порожнім або неочікуваним.
Ще одна поширена пастка — спроба за допомогою Join зробити «ліве» об’єднання, щоб у результаті залишилися всі покупці, навіть якщо у них немає замовлень. Але класичний Join працює тільки з парами, що збігаються, і не включає елементи без відповідників.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ