1. Вступ
Переходимо до цікавої й справді корисної на практиці теми: операції над множинами в LINQ — Union, Intersect, Except. Вони дають змогу працювати з колекціями так, ніби ви займаєтеся справжньою алгеброю множин (або рахуєте наліпки у двох пачках на столі програміста — погодьтеся, майже те саме). Якщо ви ніколи не чули про алгебру множин (або вже забули), це може звучати лячно. Та насправді все просто: уявіть два мішки з фруктами. Алгебра множин — це спосіб з’ясувати, які фрукти є в обох мішках, які — лише в одному, а які — разом, якщо їх об’єднати. Це як гра з наліпками: можна скласти все разом, знайти спільні або виключити одні з інших. Ось і все.
Практика: Ці методи доречні, коли потрібно об’єднати результати двох різних запитів, знайти спільні елементи або визначити різницю між колекціями. Наприклад:
- зібрати загальний список усіх товарів, які є або у списку продажів, або у списку покупок;
- знайти товари, які є і там, і там — спільні позиції;
- або визначити, які товари є на складі, але не продавалися — очевидно, залежалися.
Реальні завдання: алгебра множин трапляється всюди: фільтрація користувачів за підписками, пошук тих, хто був у різних групах, пошук унікальних замовлень чи таких, що перетинаються, порівняння результатів двох різних запитів до бази даних — і багато іншого.
2. Операція Union — об’єднання колекцій
Усе просто, тож перейдемо одразу до практики. Припустімо, у нас є два списки товарів:
List<string> warehouseProducts = new List<string> { "Молоко", "Хліб", "Сир", "Яйця" };
List<string> recentlySold = new List<string> { "Хліб", "Сир", "Салямі", "Чай" };
Що робить Union?
Union повертає унікальні елементи, які є принаймні в одній із колекцій.
По суті — це об’єднання.
var allProducts = warehouseProducts.Union(recentlySold);
foreach (var product in allProducts)
{
Console.WriteLine(product);
}
// Виведе: Молоко, Хліб, Сир, Яйця, Салямі, Чай
Ось як це виглядає візуально:
| Склад | Продано | Union (об’єднання) |
|---|---|---|
| Молоко | Хліб | Молоко |
| Хліб | Сир | Хліб |
| Сир | Салямі | Сир |
| Яйця | Чай | Яйця |
| Салямі | ||
| Чай |
У термінах алгебри множин Union — це операція «або»: дайте все, що є хоч десь. Уявіть дві коробки з різними видами чаю: якщо ви вирішите спробувати всі смаки, неважливо, що якийсь чай повторюється — ви вип’єте його лише раз.
Union автоматично прибирає дублікати (базується на реалізації Equals і GetHashCode для типу елементів). Якщо ви робите об’єднання для власних класів (наприклад, Product), переконайтеся, що ці методи для класу реалізовані коректно, інакше Union поводитиметься дивно: однакові за значенням елементи можуть вважатися різними.
Не забувайте: у результаті зберігається порядок із першої колекції, а нові — додаються наприкінці (у тому порядку, у якому вони вперше зустрілися в другій колекції).
Приклад з об’єктами
Продовжімо наш застосунок «Магазин». Маємо два списки Product:
public class Product
{
public string Name { get; set; }
public string Category { get; set; }
// Для коректної роботи Union/Intersect/Except потрібні правильні Equals і GetHashCode!
public override bool Equals(object? obj) =>
obj is Product other && Name == other.Name && Category == other.Category;
public override int GetHashCode() => HashCode.Combine(Name, Category);
}
List<Product> stock = new()
{
new Product { Name = "Молоко", Category = "Молочні" },
new Product { Name = "Хліб", Category = "Хлібобулочні" },
};
List<Product> sold = new()
{
new Product { Name = "Хліб", Category = "Хлібобулочні" },
new Product { Name = "Салямі", Category = "Ковбаси" },
};
var all = stock.Union(sold);
// Зверніть увагу: елементи не дублюються,
// навіть якщо вони з вигляду однакові, але мають бути рівні за логікою.
foreach (var p in all)
Console.WriteLine($"{p.Name} ({p.Category})");
Типова помилка: Якщо не перевизначити Equals/GetHashCode, Union вважатиме різні екземпляри з однаковими значеннями різними елементами.
3. Операція Intersect — перетин колекцій
Intersect повертає ті елементи, які присутні в усіх вихідних колекціях. Це своєрідне «і» в алгебрі множин.
Приклад
Згадаємо списки з попереднього прикладу:
var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
Console.WriteLine(product);
}
// Виведе: Хліб, Сир
Візуалізація:
| Склад | Продано | Intersect (спільне) |
|---|---|---|
| Хліб | Хліб | Хліб |
| Сир | Сир | Сир |
Де стане у пригоді?
- Ви хочете дізнатися, які товари з вашого складу були продані сьогодні.
- На співбесідах часто питають: «Як знайти перетин двох списків?» — LINQ робить це в один рядок!
- У бізнес‑застосунках перетин часто потрібен для фільтрації за сумісними ознаками.
Особливості та типові помилки
Якщо елемент трапляється кілька разів у колекції, то в результаті буде лише один екземпляр цього елемента.
Для складних об’єктів (наприклад, класу Product) знову важлива правильна реалізація Equals/GetHashCode.
Приклад з об’єктами
var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
Console.WriteLine($"{p.Name} ({p.Category})");
// Виведе тільки Хліб (Хлібобулочні)
4. Операція Except — різниця колекцій
Except повертає елементи, які є лише в першій колекції, але яких немає в другій.
Приклад
var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
Console.WriteLine(product);
}
// Виведе: Молоко, Яйця
Тобто це товари, які є на складі, але не продавалися.
Візуалізація:
| Склад | Продано | Except (лише склад) |
|---|---|---|
| Молоко | Молоко | |
| Яйця | Яйця | |
| Хліб | Хліб | (пропущено) |
| Сир | Сир | (пропущено) |
Аналогія
Це якби ви прибрали зі своєї стопки всі ті документи, які вже відправили, — залишиться тільки те, що ще лежить у вас і ніде більше.
Приклад з об’єктами
var unsold = stock.Except(sold);
foreach (var p in unsold)
Console.WriteLine($"{p.Name} ({p.Category})");
// Виведе: Молоко (Молочні)
! Неочевидні моменти
Метод Except чутливий до порядку: A.Except(B) — це не те саме, що B.Except(A)! Перша колекція — «звідки віднімаємо», друга — «що видаляємо».
5. Об’єднання та композиція множинних операцій з LINQ
Бувають ситуації, коли однієї операції замало. Наприклад, потрібно вивести товари, які є або тільки на складі, або тільки в проданих — але не в обох колекціях одночасно (так звана «симетрична різниця»).
Симетрична різниця («XOR» для множин):
var onlyInOne = warehouseProducts.Except(recentlySold)
.Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
Console.WriteLine(product);
}
// Виведе: Молоко, Яйця, Салямі, Чай
Для складних сценаріїв зручно комбінувати методи LINQ:
// Знайти товари, яких немає ні на складі, ні серед проданих, а тільки в списку «очікується надходження»
List<string> expected = new() { "Кава", "Чай", "Молоко" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
Console.WriteLine(product);
// Виведе: Кава
6. Робота з власними типами та IEqualityComparer
Іноді порівнювати об’єкти за всіма полями не потрібно: наприклад, вам важлива тільки назва товару, а категорія — уже не суттєва. Для цього методи LINQ підтримують додатковий параметр — IEqualityComparer<T>, який визначає, як порівнювати елементи.
Приклад власного порівнювача:
class ProductNameComparer : IEqualityComparer<Product>
{
public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}
var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
Console.WriteLine(p.Name); // Молоко, Хліб, Салямі
Це особливо зручно, якщо ви не хочете змінювати Equals/GetHashCode у всій моделі, а лише разово порівняти об’єкти за специфічним правилом.
7. Візуальні схеми та таблиці
Узагальнімо застосування (для списків A і B):
| Операція | Результат |
|---|---|
|
Усе, що є в A або B (унікальні елементи). |
|
Усе, що є в обох (A і B). |
|
Усе, що є тільки в A, але не в B. |
Схема (діаграма Венна, якби ми могли намалювати):
[A] [B]
oooooooooo
oooooooo oooo
ooooo oo ooo
ooo ooo oo
oo o ooo
ooo ooo ooo
- Union: усе, що належить принаймні одному з кіл.
- Intersect: тільки перетин (центр).
- Except: тільки те, що в колі A, не чіпаючи перетин із B.
8. Типові помилки під час використання Union, Intersect і Except
Помилка № 1: використання з об’єктами без Equals і GetHashCode.
Якщо у вашого класу не перевизначені ці методи, методи Union, Intersect і Except працюватимуть некоректно: однакові за змістом об’єкти вважатимуться різними. У результаті ви отримаєте неочікуваний (і часто марний) результат.
Помилка № 2: спроба порівнювати об’єкти за частиною полів без IEqualityComparer.
Наприклад, якщо ви хочете порівнювати товари тільки за назвою, а не за всією структурою, сам по собі Intersect цього не зрозуміє. Без явного IEqualityComparer результат не збігатиметься з очікуваннями.
Помилка № 3: неправильні очікування щодо порядку елементів.
Багато хто думає, що підсумкова колекція зберігає порядок об’єднання чи перетину. Але поведінка залежить від методу: Union зберігає порядок першої колекції, а Intersect і Except можуть повернути елементи в непередбачуваному порядку. Краще взагалі не покладатися на порядок.
Помилка № 4: ігнорування продуктивності під час роботи з великими колекціями.
Якщо даних багато, методи можуть працювати повільно. Подумайте про попередню агрегацію, фільтрацію або використання хеш‑структур (наприклад, HashSet), щоб пришвидшити операції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ