Avatar ·

LINQ Distinct() по конкретному свойству объекта

Привет, коллеги! Я новичок в C# и LINQ, и столкнулся с такой проблемой. У меня есть список объектов класса `Product`: ```csharp public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } ``` Я заполняю список данными, и в нём могут быть дубликаты по полю `Name`. Например: ```csharp var products = new List { new Product { Id = 1, Name = "Apple", Price = 100 }, new Product { Id = 2, Name = "Banana", Price = 50 }, new Product { Id = 3, Name = "Apple", Price = 120 }, new Product { Id = 4, Name = "Cherry", Price = 200 } }; ``` Я хочу получить список уникальных продуктов только по свойству `Name`, чтобы остались, например, только "Apple", "Banana", "Cherry" (без разницы, какой из дубликатов Apple останется — первый или последний). Метод `Distinct()` по умолчанию сравнивает объекты по ссылке, а не по свойству. Я пробовал переопределить `Equals` и `GetHashCode`, но это кажется костылём, и не хочется менять класс ради одной операции. Подскажите, как правильно и элегантно сделать Distinct по одному полю с помощью LINQ? Желательно без сторонних библиотек. Спасибо! <<>>

Решение через GroupBy и First

Стандартный LINQ не содержит метода DistinctBy (он появится только в .NET 6), но мы можем легко эмулировать эту функциональность с помощью комбинации GroupBy и First. Это самый популярный и читаемый способ.

var uniqueByName = products
    .GroupBy(p => p.Name)
    .Select(g => g.First())
    .ToList();

Как это работает:

  • GroupBy(p => p.Name) — группирует все продукты по значению свойства Name. В результате получается коллекция групп, где ключ — это имя продукта, а значение — все продукты с таким именем.
  • Select(g => g.First()) — из каждой группы берёт первый элемент. Таким образом, для каждого уникального имени остаётся только один продукт.
  • ToList() — материализует результат в список.

Результат для вашего примера: будут выбраны продукты с Id = 1 (Apple), 2 (Banana), 4 (Cherry). Если вы хотите последний дубликат, замените First() на Last().

Универсальный метод-расширение

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

public static class EnumerableExtensions
{
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector)
    {
        var seenKeys = new HashSet<TKey>();
        foreach (var element in source)
        {
            if (seenKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }
}

Использование:

var uniqueByName = products.DistinctBy(p => p.Name).ToList();

Этот метод работает эффективнее GroupBy, так как не создаёт промежуточные группы, а использует HashSet для отслеживания уже встреченных ключей. Он сохраняет порядок элементов и возвращает первое вхождение каждого уникального ключа.

Важное замечание

Начиная с .NET 6, в LINQ появился встроенный метод DistinctBy:

var uniqueByName = products.DistinctBy(p => p.Name).ToList();

Если ваш проект использует .NET 6 или новее, это самый простой и производительный вариант.

<<>>

Альтернативный подход: использование MoreLINQ или собственного IEqualityComparer

Если вы не хотите писать свой метод-расширение, можно воспользоваться сторонней библиотекой MoreLINQ (устанавливается через NuGet). Она содержит метод DistinctBy:

using MoreLinq;

var uniqueByName = products.DistinctBy(p => p.Name).ToList();

Это решение полностью аналогично самописному расширению, но не требует его поддержки.

Решение через Distinct с кастомным компаратором

Другой вариант — реализовать IEqualityComparer<Product>:

public class ProductNameComparer : IEqualityComparer<Product>
{
    public bool Equals(Product x, Product y) => 
        x?.Name == y?.Name;

    public int GetHashCode(Product obj) => 
        obj.Name?.GetHashCode() ?? 0;
}

// Использование:
var uniqueByName = products.Distinct(new ProductNameComparer()).ToList();

Этот способ менее гибкий (компаратор привязан к конкретному классу и свойству), но может быть полезен, если вы часто фильтруете по Name в разных местах кода.

Рекомендация: Для разовых операций используйте GroupBy + First. Для частого использования — напишите универсальный DistinctBy или подключите MoreLINQ. Избегайте переопределения Equals в самом классе, если это не соответствует его бизнес-логике.