Відмінність () з лямбда?


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
Дякуємо за оновлення Anders Hejlsberg!
Tor Haugen

Ні, це не має сенсу - як два об'єкти, що містять однакові значення, можуть повернути два різних хеш-коди ??
GY

Це може допомогти - рішення для .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId))та пояснити, чому GetHashCode () важливо правильно працювати.
marbel82

Пов’язаний / можливий дублікат: Відмінність LINQ () про певну власність
Marc.2377,

Відповіді:


1028
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: Це не зовсім те саме. Це дає вам лише ідентифікатори клієнта. Я хочу всього клієнта :)
ryanman

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 # на глибині», щоб досягти богоподібних знань про посилання
nocarrier

2
чудова відповідь !!! Крім того, для всіх VB_Complainers про yield+ додаткову lib, foreach можна переписати якreturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

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
(Просто для пояснення мого коментаря щодо хеш-коду - з цим кодом дуже просто закінчити рівне (x, y) == true, але GetHashCode (x)! = GetHashCode (y) .)
Джон Скіт

Я згоден із запереченням хеш-коду. Все-таки +1 для шаблону.
Tor Haugen

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

1
@JaredPar: Саме так. Хеш-код повинен відповідати функції рівності, яку ви використовуєте, імовірно, це не за замовчуванням, інакше ви б не турбувались :) Ось чому я вважаю за краще використовувати проекцію - ви можете отримати рівність і розумний хеш код таким чином. Це також робить код виклику меншим тиражуванням. Правда, це працює лише у випадках, коли ви хочете двічі однакову проекцію, але це кожен випадок, який я бачив на практиці :)
Джон Скіт

18

Скорочене рішення

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

1
Чи можете ви додати трохи пояснень, чому це покращується?
Кіт Пінсон

Це насправді добре працювало для мене, коли цього не робив Конрад.
неописатись

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.
toddmo

Але, мені довелося змінити , 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
Сюди прийшов сказати це. ЦЕ має бути прийнятою відповіддю
Тоні

5
Ні, це не має бути прийнятою відповіддю, якщо тільки все, що ви хочете, є різними значеннями користувацької властивості. Загальне питання ОП полягало в тому, як повернути окремі об'єкти на основі конкретної властивості об'єкта.
tomo

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
Чи можу я вказати свій явний компаратор типу вбудований?


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 distctResults = від c1 у myList join c2 у myList на


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"))
        );

Примітка. Це також дозволить вам вказати інші функції, наприклад, наприклад .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.