Как в C# с помощью LINQ получить элементы из одного списка, которых нет в другом списке?
Использование 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.
Ответов пока нет.
Войдите чтобы оставить ответ