JavaRush /Курси /C# SELF /Операції Union,

Операції Union, Intersect, Except у LINQ

C# SELF
Рівень 33 , Лекція 3
Відкрита

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.Union(B)
Усе, що є в A або B (унікальні елементи).
A.Intersect(B)
Усе, що є в обох (A і B).
A.Except(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), щоб пришвидшити операції.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ