Avatar ·

Как в C# с помощью LINQ получить элементы из одного списка, которых нет в другом списке?

Всем привет! Я начинающий C# разработчик, и у меня возникла проблема при работе с коллекциями. Есть два списка объектов одного типа. Допустим, у меня есть класс Product с полями Id и Name. Первый список (listA) содержит все продукты из базы данных, а второй (listB) — это список продуктов, которые пользователь уже выбрал для заказа. Мне нужно получить третий список (listC), который будет содержать только те продукты из listA, которых нет в listB. Я пробовал использовать цикл foreach и проверять Contains, но это выглядит громоздко и некрасиво. Плюс, когда списки большие, такой код работает медленно. Слышал, что LINQ позволяет делать такие вещи элегантно и быстро. Подскажите, как правильно написать запрос, чтобы получить элементы, отсутствующие во втором списке? Важно, чтобы сравнение происходило по Id, так как Name может повторяться. Пример моей структуры данных: List listA = new List { new Product { Id = 1, Name = "Молоко" }, new Product { Id = 2, Name = "Хлеб" }, new Product { Id = 3, Name = "Яйца" } }; List listB = new List { new Product { Id = 2, Name = "Хлеб" }, new Product { Id = 4, Name = "Сыр" } }; Мне нужно получить listC = { Product(1, "Молоко"), Product(3, "Яйца") }. Заранее спасибо за помощь! <<>>

Использование LINQ Except с кастомным компаратором

Ваша задача решается с помощью метода Except(), который возвращает элементы первой последовательности, отсутствующие во второй. Но есть нюанс: по умолчанию Except сравнивает объекты по ссылке (ReferenceEquals), поэтому для сравнения по Id нужно либо реализовать IEquatable, либо написать собственный компаратор.

Вот наиболее чистый и производительный способ:

public class Product : IEquatable
{
    public int Id { get; set; }
    public string Name { get; set; }

    public bool Equals(Product other)
    {
        if (other is null) return false;
        return this.Id == other.Id;
    }

    public override bool Equals(object obj) => Equals(obj as Product);
    public override int GetHashCode() => Id.GetHashCode();
}

// Использование
var listC = listA.Except(listB).ToList();

Теперь listC будет содержать { Product(1, "Молоко"), Product(3, "Яйца") }.

Почему это работает?

  • Equals() определяет логику сравнения (по Id).
  • GetHashCode() — обязателен для работы HashSet внутри Except. Если не переопределить, производительность упадет.
  • Метод Except использует хеш-таблицу, поэтому сложность O(n+m), а не O(n*m) как у вложенных циклов.

Альтернатива без изменения класса (через компаратор)

Если вы не можете менять класс Product (например, он из внешней библиотеки), используйте кастомный компаратор:

public class ProductIdComparer : IEqualityComparer
{
    public bool Equals(Product x, Product y)
    {
        if (x is null || y is null) return false;
        return x.Id == y.Id;
    }

    public int GetHashCode(Product obj) => obj.Id.GetHashCode();
}

// Использование
var listC = listA.Except(listB, new ProductIdComparer()).ToList();

Важные замечания

  • Оба подхода работают в .NET 3.5 и выше (LINQ to Objects).
  • Если списки очень большие (>100k элементов), Except будет быстрее, чем Where + Any.
  • Если нужно сохранить порядок элементов из listA, Except его сохраняет.

Этот паттерн часто используется для синхронизации данных, фильтрации выбранных элементов и в задачах миграции. Рекомендую привыкнуть к переопределению Equals/GetHashCode для бизнес-объектов — это окупается во многих сценариях.

<<>>

Решение через Where и Any (более читаемое для новичков)

Если вы не хотите заморачиваться с компараторами, можно использовать простой LINQ-запрос:

var listC = listA
    .Where(a => !listB.Any(b => b.Id == a.Id))
    .ToList();

Этот код проверяет для каждого элемента из listA, есть ли в listB элемент с таким же Id. Если нет — добавляет в результат.

Плюсы и минусы

  • Плюс: не требует изменения класса Product или создания дополнительных компараторов.
  • Минус: сложность O(n*m) — для каждого элемента listA перебирается весь listB. При больших списках (10k+ элементов) заметно тормозит.

Когда использовать

Этот подход отлично подходит для:

  • Небольших коллекций (до 1000 элементов).
  • Быстрых прототипов и тестового кода.
  • Сценариев, где читаемость важнее производительности.

Для production-кода с большими данными рекомендую первый вариант с Except — он и быстрее, и лаконичнее после настройки.

Еще один лайфхак: если сравниваете по простому типу (int, string), можно использовать HashSet:

var idsInB = new HashSet(listB.Select(b => b.Id));
var listC = listA.Where(a => !idsInB.Contains(a.Id)).ToList();

Это даст скорость O(n+m) без изменения класса Product.