Как в LINQ применить Distinct() только по одному свойству объекта?
Решение с помощью GroupBy и Select
Самый простой и интуитивно понятный способ — сгруппировать элементы по нужному свойству, а затем взять первый элемент из каждой группы:
var distinctByName = products
.GroupBy(p => p.Name)
.Select(g => g.First())
.ToList();
Этот код:
- Группирует все продукты по полю
Name - Из каждой группы выбирает первый элемент (можно также
Last()или кастомную логику) - Возвращает список уникальных по имени продуктов
Результат: объекты с Id = 1 (Ноутбук), 2 (Мышь), 4 (Книга).
Создание собственного EqualityComparer
Более продвинутый, но гибкий подход — реализовать IEqualityComparer<T> и передать его в Distinct():
public class ProductNameComparer : IEqualityComparer<Product>
{
public bool Equals(Product x, Product y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Name == y.Name;
}
public int GetHashCode(Product obj)
{
return obj.Name?.GetHashCode() ?? 0;
}
}
// Использование:
var distinctByName = products.Distinct(new ProductNameComparer()).ToList();
Преимущества: можно переиспользовать компаратор в разных местах, легко добавить дополнительные поля для сравнения. Недостаток: нужно писать отдельный класс.
Расширение MoreLINQ — DistinctBy
Если вы используете библиотеку MoreLINQ (NuGet: morelinq), то задача решается одной строкой:
var distinctByName = products.DistinctBy(p => p.Name).ToList();
Это самый лаконичный вариант. Метод DistinctBy принимает функцию селектора ключа и возвращает элементы с уникальными значениями этого ключа (сохраняя первый встреченный элемент).
Сравнение подходов
- GroupBy + First — работает «из коробки», легко читается, но создаёт промежуточные группы. Для больших коллекций может быть чуть медленнее.
- Свой компаратор — максимально гибко, но требует кода. Подходит для частого использования.
- DistinctBy — идеально, если можно добавить зависимость. Чисто, быстро, эффективно.
Для большинства случаев я рекомендую GroupBy — он не требует внешних библиотек и интуитивно понятен даже новичкам.
Важное замечание
Все эти методы используют первый встреченный элемент из группы одинаковых по ключу объектов. Если вам нужен, например, самый дешёвый или самый новый — используйте GroupBy с кастомной агрегацией:
var cheapestByName = products
.GroupBy(p => p.Name)
.Select(g => g.OrderBy(p => p.Price).First())
.ToList();
<<Альтернативный подход: HashSet и ручная фильтрация
Если вам не нравится группировка, а компараторы кажутся избыточными, можно использовать HashSet для отслеживания уже встреченных ключей:
var seenNames = new HashSet<string>();
var distinctByName = new List<Product>();
foreach (var product in products)
{
if (seenNames.Add(product.Name))
{
distinctByName.Add(product);
}
}
Этот код:
- Проходит по коллекции один раз (O(n))
- Добавляет имя в
HashSet— если имя уже есть,Addвернётfalse - Если имя уникальное — добавляет объект в результат
Преимущества: максимальная производительность, нет накладных расходов на группировку или компараторы. Недостаток: это императивный код, а не декларативный LINQ.
Когда использовать
- Когда важна скорость на больших коллекциях (миллионы записей)
- Когда нужно выполнить дополнительную логику при первом появлении ключа
- Когда вы пишете код для .NET Framework без доступа к MoreLINQ
Этот подход легко обернуть в extension-метод для повторного использования:
public static IEnumerable<T> DistinctBy<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> keySelector)
{
var seenKeys = new HashSet<TKey>();
foreach (var element in source)
{
if (seenKeys.Add(keySelector(element)))
{
yield return element;
}
}
}
Так вы получите свой собственный DistinctBy без внешних зависимостей.