Отличаться () с лямбда?


746

Правильно, поэтому у меня есть перечислимое и я хочу получить от него различные значения.

Используя System.Linq, конечно, есть метод расширения под названием Distinct. В простом случае его можно использовать без параметров, например:

var distinctValues = myStringList.Distinct();

Хорошо, но если у меня есть множество объектов, для которых мне нужно указать равенство, единственная доступная перегрузка:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Аргумент сравнения равенства должен быть экземпляром IEqualityComparer<T>. Я могу сделать это, конечно, но это несколько многословно и, ну, в общем, грязно.

То, что я ожидал бы, является перегрузкой, которая взяла бы лямбду, скажем Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Кто-нибудь знает, существует ли какое-то такое расширение или какой-то эквивалентный обходной путь? Или я что-то упустил?

В качестве альтернативы, есть ли способ указания встроенного IEqualityComparer (смущать меня)?

Обновить

Я нашел ответ Андерса Хейлсберга на пост на форуме MSDN на эту тему. Он говорит:

Проблема, с которой вы столкнетесь, состоит в том, что, когда два объекта сравниваются одинаково, они должны иметь одинаковое возвращаемое значение GetHashCode (иначе хеш-таблица, используемая внутри Distinct, не будет работать правильно). Мы используем IEqualityComparer, потому что он упаковывает совместимые реализации Equals и GetHashCode в единый интерфейс.

Я полагаю, это имеет смысл ..


2
см. stackoverflow.com/questions/1183403/… для решения, использующего GroupBy

17
Спасибо за обновление Андерса Хейлсберга!
Тор Хауген

Нет, это не имеет смысла - как два объекта, которые содержат одинаковые значения, могут возвращать два разных хеш-кода?
GY

Это могло бы помочь - решение для .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), и объяснить , почему GetHashCode () имеет важное значение для правильной работы.
marbel82

Связанные / возможные дубликаты: LINQ's Distinct () для определенного свойства
Marc.2377

Ответы:


1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());

12
Отлично! Это действительно легко инкапсулировать в метод расширения, например DistinctBy(или даже Distinct, поскольку подпись будет уникальной).
Томас Ашан

1
Не работает для меня! <Метод «Первый» может использоваться только в качестве конечной операции запроса. Попробуйте вместо этого использовать метод «FirstOrDefault».> Даже я пытался «FirstOrDefault», он не работал.
JatSing

63
@ TorHaugen: Просто знайте, что создание всех этих групп сопряжено с определенными затратами. Это не может передавать поток данных и в конечном итоге буферизует все данные, прежде чем что-либо вернуть. Конечно, это может не относиться к вашей ситуации, но я предпочитаю элегантность DistinctBy :)
Джон Скит

2
@JonSkeet: Это достаточно для программистов VB.NET, которые не хотят импортировать дополнительные библиотеки только для одной функции. Без ASync CTP VB.NET не поддерживает yieldоператор, поэтому потоковая передача технически невозможна. Спасибо за ваш ответ, хотя. Я буду использовать его при кодировании в C #. ;-)
Алекс Эссильфи

2
@BenGripka: Это не совсем то же самое. Это только дает вам идентификаторы клиентов. Я хочу, чтобы весь клиент :)
Райанман

496

Мне кажется, что ты хочешь DistinctByот MoreLINQ . Вы можете написать:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Вот урезанная версия DistinctBy(без проверки недействительности и без возможности указать свой собственный ключ сравнения):

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

14
Я знал, что лучший ответ будет опубликован Джоном Скитом, просто прочитав название поста. Если это как-то связано с LINQ, Скит твой человек. Прочитайте «C # In Depth», чтобы получить знание о Боге, подобное Linq.
нокарьер

2
отличный ответ !!! Кроме того, для всех VB_Complainers о yield+ extra lib, foreach может быть переписан какreturn source.Where(element => knownKeys.Add(keySelector(element)));
Денис Морозов

5
@ sudhAnsu63 это ограничение LinqToSql (и других провайдеров linq). Цель LinqToX состоит в том, чтобы перевести ваше лямбда-выражение C # в собственный контекст X. То есть LinqToSql преобразует ваш C # в SQL и выполняет эту команду по возможности везде. Это означает, что любой метод, находящийся в C #, не может быть «пропущен» через linqProvider, если нет способа выразить его в SQL (или любом другом поставщике linq, который вы используете). Я вижу это в методах расширения для преобразования объектов данных для просмотра моделей. Вы можете обойти это, "материализуя" запрос, вызывая ToList () перед DistinctBy ().
Майкл Блэкберн

1
И всякий раз, когда я возвращаюсь к этому вопросу, я продолжаю задаваться вопросом, почему они не принимают хотя бы часть MoreLinq в BCL.
Шимми Вайцхандлер

2
@Shimmy: Я бы, конечно, приветствовал это ... Я не уверен, какова целесообразность. Я могу поднять его в .NET Foundation, хотя ...
Джон Скит

39

Обернуть вещи . Я думаю, что большинство людей, которые пришли сюда, как я, хотят простейшего решения без использования каких-либо библиотек и с максимально возможной производительностью .

(Принятая группа по методу для меня, я думаю, является излишним с точки зрения производительности.)

Вот простой метод расширения, использующий интерфейс IEqualityComparer, который работает также для нулевых значений.

Применение:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Код метода расширения

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}

19

Нет, такой перегрузки метода расширения для этого нет. Я находил это разочаровывающим в прошлом и поэтому обычно пишу вспомогательный класс для решения этой проблемы. Цель состоит в том, чтобы преобразовать Func<T,T,bool>в IEqualityComparer<T,T>.

пример

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Это позволяет вам написать следующее

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));

8
Это имеет неприятную реализацию хеш-кода, хотя. Это проще создать IEqualityComparer<T>из проекции: stackoverflow.com/questions/188120/…
Джон Скит

7
(Просто, чтобы объяснить мой комментарий о хеш-коде - с этим кодом очень легко получить Equals (x, y) == true, но GetHashCode (x)! = GetHashCode (y). Это в основном нарушает что-либо вроде хеш-таблицы .)
Джон Скит

Я согласен с возражением по хэш-коду. Тем не менее, +1 за образец.
Тор Хауген

@Jon, да, я согласен, оригинальная реализация GetHashcode не оптимальна (была ленива). Я переключил это, чтобы по существу использовать теперь EqualityComparer <T> .Default.GetHashcode (), который немного более стандартен. Правда, единственная гарантированная работа реализации GetHashcode в этом сценарии - просто вернуть постоянное значение. Убивает поиск по хеш-таблице, но гарантированно будет функционально корректным.
JaredPar

1
@JaredPar: Точно. Хеш-код должен соответствовать используемой вами функции равенства, которая, по-видимому , не является используемой по умолчанию, иначе вы бы не стали беспокоиться :) Вот почему я предпочитаю использовать проекцию - вы можете получить как равенство, так и разумный хеш код таким образом. Это также делает вызывающий код менее дублирующимся. По общему признанию, это работает только в тех случаях, когда вы хотите один и тот же прогноз дважды, но это каждый случай, который я видел на практике :)
Джон Скит

18

Сокращенное решение

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());

1
Не могли бы вы объяснить, почему это улучшилось?
Кит Пинсон

На самом деле это хорошо сработало, когда у Конрада нет.
neoscribe

13

Это будет делать то, что вы хотите, но я не знаю о производительности:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

По крайней мере, это не многословно.


12

Вот простой метод расширения, который делает то, что мне нужно ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

Жаль, что они не внедрили какой-то особый метод в рамки, но эй хо.


это лучшее решение без добавления этой библиотеки morelinq.
10:10

Но, мне пришлось изменить , x.Keyчтобы x.First()и изменить возвращаемое значениеIEnumerable<T>
toddmo

@toddmo Спасибо за отзыв :-) Да, звучит логично ... Я обновлю ответ после дальнейшего изучения.
Дэвид Киркланд,

1
никогда не поздно сказать спасибо за решение, простое и чистое
Али

4

Что-то, что я использовал, и это хорошо для меня.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

@Mukus Я не уверен, почему вы спрашиваете об имени класса здесь. Мне нужно было назвать класс как-нибудь, чтобы реализовать IEqualityComparer, поэтому я просто поставил префикс My.
Kleinux

4

Все решения, которые я видел здесь, основаны на выборе уже сопоставимого поля. Однако, если нужно сравнивать по-другому, то здесь это решение, как правило, работает для чего-то вроде:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()

Что такое LambdaComparer, откуда вы это импортируете?
Патрик Грэм

@PatrickGraham ссылается в ответе: brendan.enrick.com/post/…
Дмитрий Леденцов

3

Возьми другой путь:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

Последовательность, возвращающая различные элементы, сравнивает их по свойству _myCaustomerProperty.


1
Пришел сюда, чтобы сказать это. ЭТО должен быть принятый ответ
Still.Tony

5
Нет, это не должен быть принятый ответ, если только вам не нужны отдельные значения настраиваемого свойства. Общий OP-вопрос заключался в том, как вернуть отдельные объекты на основе определенного свойства объекта.
Томо

2

Вы можете использовать InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

    public bool Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

Образец использования :

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Источник: https://stackoverflow.com/a/5969691/206730
Использование IEqualityComparer для Union
Могу ли я указать свой явный компаратор типа inline?


2

Вы можете использовать LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

        public bool Equals(T x, T y)
        {
            return _equalsFunction(x, y);
        }

        public int GetHashCode(T obj)
        {
            return obj.GetHashCode();
        }

        private readonly Func<T, T, bool> _equalsFunction;
    }

1

Хитрый способ сделать это - использовать Aggregate()расширение, используя словарь в качестве аккумулятора со значениями свойства ключа в качестве ключей:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

И решение в стиле GroupBy использует ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());

Хорошо, но почему бы просто не создать Dictionary<int, Customer>вместо этого?
ruffin

0

Я предполагаю, что у вас есть IEnumerable, и в вашем примере делегата вы хотели бы, чтобы c1 и c2 ссылались на два элемента в этом списке?

Я полагаю, что вы могли бы достичь этого с помощью самостоятельного объединения var varResults = from c1 в myList join c2 в myList on


0

Если Distinct()не дает уникальных результатов, попробуйте это:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);

0

В пакете Microsoft System.Interactive имеется версия Distinct, в которой используется лямбда-ключ выбора. По сути, это то же самое, что и решение Джона Скита, но оно может помочь людям узнать и проверить остальную часть библиотеки.


0

Вот как вы можете это сделать:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Этот метод позволяет вам использовать его, указав один параметр как .MyDistinct(d => d.Name), но он также позволяет вам указывать наличие условия в качестве второго параметра, например, так:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

NB. Это также позволит вам указать другие функции, например, например .LastOrDefault(...).


Если вы хотите показать только условие, вы можете сделать его еще проще, реализовав его следующим образом:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

В этом случае запрос будет выглядеть так:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NB. Здесь выражение проще, но примечание .MyDistinct2использует .FirstOrDefault(...)неявно.


Примечание. В приведенных выше примерах используется следующий демонстрационный класс.

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};

0

IEnumerable лямбда-расширение:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Применение:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.