Как использовать Distinct() с лямбда-выражением для фильтрации дубликатов по свойству объекта?
Всем привет! Я новичок в C# и столкнулся с проблемой при работе с коллекциями. У меня есть список объектов класса Person:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
Я заполняю список данными, и в нём оказываются дубликаты по полю Name. Мне нужно получить только уникальные имена, но при этом сохранить объекты целиком (или хотя бы выбрать один из дубликатов).
Я знаю, что есть метод Distinct(), но он работает только для примитивных типов или если переопределить Equals и GetHashCode в классе. А я не хочу менять класс Person — он используется в других местах, и сравнение по всем полям мне не подходит.
Я видел, что в LINQ есть перегрузка Distinct(IEqualityComparer), но писать отдельный класс компаратора для каждого случая кажется слишком громоздким. Можно ли как-то передать лямбда-выражение прямо в Distinct(), чтобы указать, по какому свойству сравнивать? Например, что-то вроде list.Distinct(p => p.Name)?
Подскажите, как элегантно решить эту задачу без лишнего кода. Заранее спасибо!
<<Решение: создаём универсальный компаратор с лямбдой
К сожалению, встроенный метод Distinct() не принимает лямбда-выражение напрямую. Но мы можем написать небольшой вспомогательный класс LambdaEqualityComparer, который принимает функцию-селектор ключа. Это делается один раз, а затем используется в любом месте проекта.
Вот полный пример:
public class LambdaEqualityComparer<T, TKey> : IEqualityComparer<T>
{
private readonly Func<T, TKey> _keySelector;
public LambdaEqualityComparer(Func<T, TKey> keySelector)
{
_keySelector = keySelector;
}
public bool Equals(T x, T y)
{
if (x == null && y == null) return true;
if (x == null || y == null) return false;
return EqualityComparer<TKey>.Default.Equals(_keySelector(x), _keySelector(y));
}
public int GetHashCode(T obj)
{
if (obj == null) return 0;
return EqualityComparer<TKey>.Default.GetHashCode(_keySelector(obj));
}
}
Теперь вы можете использовать его так:
var people = new List<Person>
{
new Person { Id = 1, Name = "Alice", Age = 30 },
new Person { Id = 2, Name = "Bob", Age = 25 },
new Person { Id = 3, Name = "Alice", Age = 35 } // дубликат по Name
};
// Получаем уникальных людей по имени (останется первый встреченный)
var uniqueByName = people
.Distinct(new LambdaEqualityComparer<Person, string>(p => p.Name))
.ToList();
foreach (var person in uniqueByName)
{
Console.WriteLine($"{person.Id}: {person.Name}, {person.Age}");
}
// Вывод: 1: Alice, 30
// 2: Bob, 25
Ещё более элегантный вариант — extension-метод
Чтобы синтаксис был совсем лаконичным, можно добавить метод расширения:
public static class EnumerableExtensions
{
public static IEnumerable<T> DistinctBy<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> keySelector)
{
return source.Distinct(new LambdaEqualityComparer<T, TKey>(keySelector));
}
}
// Использование:
var uniqueByName = people.DistinctBy(p => p.Name).ToList();
Важные замечания
- Порядок элементов:
Distinct()сохраняет порядок исходной последовательности, возвращая первый встреченный элемент из каждой группы дубликатов. - Null-значения: компаратор корректно обрабатывает
nullкак для самих объектов, так и для ключей. - Производительность: метод использует хэш-таблицу внутри, поэтому работает за O(n) в среднем.
Если вы используете .NET 6 или новее, то можете воспользоваться встроенным методом DistinctBy() из LINQ — он делает то же самое!
// .NET 6+
var uniqueByName = people.DistinctBy(p => p.Name).ToList();
Таким образом, у вас есть несколько вариантов: написать свой компаратор один раз, использовать extension-метод или перейти на современную версию .NET, где это уже встроено.
<<Альтернативный подход: группировка с последующим выбором первого элемента
Если вы не хотите писать вспомогательные классы, можно обойтись стандартными методами LINQ — GroupBy() и Select(). Это менее производительно (O(n log n) из-за группировки), но работает без дополнительного кода:
var uniqueByName = people
.GroupBy(p => p.Name)
.Select(g => g.First())
.ToList();
Этот подход делает то же самое: группирует людей по имени, а затем из каждой группы берёт первого встреченного. Результат будет идентичен DistinctBy().
Плюсы:
- Не требует написания дополнительных классов или методов.
- Интуитивно понятно даже новичкам.
Минусы:
- Медленнее на больших коллекциях (группировка требует сортировки или хэширования с дополнительными накладными расходами).
- Код чуть длиннее, чем
DistinctBy().
Этот вариант хорошо подходит для небольших списков (до нескольких тысяч элементов) или когда вы не хотите создавать вспомогательные утилиты. Для серьёзных проектов рекомендую всё-таки использовать DistinctBy — либо встроенный, либо написанный вручную, как в первом ответе.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги