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.

Схема (Venn Diagram, если бы мы могли рисовать):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: всё, что внутри обоих кругов.
  • Intersect: только пересечение (центр).
  • Except: только то, что в круге А, не затрагивая пересечения с В.

8. Типичные ошибки при использовании Union, Intersect и Except

Ошибка №1: использование с объектами без Equals и GetHashCode.
Если у вашего класса не переопределены эти методы, методы Union, Intersect и Except будут работать некорректно: одинаковые по содержанию объекты будут считаться разными. В результате вы получите неожиданный (и часто бесполезный) результат.

Ошибка №2: попытка сравнивать объекты по части полей без IEqualityComparer.
Например, если вы хотите сравнивать товары только по названию, а не по всей структуре, просто так Intersect это не поймёт. Без явного IEqualityComparer результат не совпадёт с ожиданиями.

Ошибка №3: неверные ожидания по поводу порядка элементов.
Многие предполагают, что итоговая коллекция сохраняет порядок объединения или пересечения. Но поведение зависит от метода: Union сохраняет порядок первой коллекции, но Intersect и Except могут вернуть элементы в непредсказуемом порядке. Лучше не полагаться на порядок вовсе.

Ошибка №4: игнорирование производительности при работе с большими коллекциями.
Если данные объёмные, методы могут работать медленно. Подумайте о предварительной агрегации, фильтрации или использовании хеш-структур (например, HashSet), чтобы ускорить операции.

2
Задача
C# SELF, 33 уровень, 3 лекция
Недоступна
Использование Intersect для нахождения общих элементов
Использование Intersect для нахождения общих элементов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ