Wyraźny () z lambda?


746

Tak, więc mam wyliczenie i chcę uzyskać od niego odrębne wartości.

Używając System.Linq, istnieje oczywiście metoda rozszerzenia o nazwie Distinct. W prostym przypadku można go używać bez parametrów, takich jak:

var distinctValues = myStringList.Distinct();

Dobrze i dobrze, ale jeśli mam wyliczenie obiektów, dla których muszę określić równość, jedynym dostępnym przeciążeniem jest:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Argument porównania równości musi być instancją klasy IEqualityComparer<T>. Mogę to oczywiście zrobić, ale jest to trochę gadatliwe i, no cóż, niezgrabne.

Spodziewałbym się przeciążenia, które zajęłoby lambda, powiedzmy Func <T, T, bool>:

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

Czy ktoś wie, czy istnieje jakieś takie rozszerzenie lub jakieś równoważne obejście? A może coś mi brakuje?

Alternatywnie, czy istnieje sposób na określenie inline IEqualityComparer (zawstydza mnie)?

Aktualizacja

Znalazłem odpowiedź Andersa Hejlsberga na post na forum MSDN na ten temat. On mówi:

Problem, na który natkniesz się, polega na tym, że gdy dwa obiekty są równe, muszą mieć tę samą wartość zwracaną GetHashCode (w przeciwnym razie tabela skrótów używana wewnętrznie przez Distinct nie będzie działać poprawnie). Używamy IEqualityComparer, ponieważ łączy on kompatybilne implementacje Equals i GetHashCode w jednym interfejsie.

Myślę, że to ma sens ...


2
zobacz stackoverflow.com/questions/1183403/... dla rozwiązania używającego GroupBy

17
Dzięki za aktualizację Andersa Hejlsberga!
Tor Haugen,

Nie, to nie ma sensu - w jaki sposób dwa obiekty zawierające identyczne wartości mogą zwrócić dwa różne kody skrótu?
GY

To może pomóc - rozwiązanie dla .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), i wyjaśnić, dlaczego GetHashCode () jest ważne, aby działać poprawnie.
marbel82,

Odpowiedzi:


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

12
Świetny! Jest to naprawdę łatwe do kapsułkowania w metodzie rozszerzenia, takiej jak DistinctBy(a nawet Distinct, ponieważ podpis będzie unikalny).
Tomas Aschan

1
Nie działa dla mnie! <Metodę „First” można wykorzystać tylko jako ostatnią operację zapytania. Zastanów się nad użyciem metody „FirstOrDefault” w tym przypadku.> Nawet próbowałem „FirstOrDefault”, to nie działało.
JatSing,

63
@TorHaugen: Pamiętaj, że utworzenie wszystkich tych grup wiąże się z pewnymi kosztami. To nie może przesyłać strumieniowo danych wejściowych i ostatecznie buforuje wszystkie dane przed zwróceniem czegokolwiek. To może oczywiście nie mieć znaczenia dla twojej sytuacji, ale wolę elegancję DistinctBy :)
Jon Skeet

2
@JonSkeet: Jest to wystarczająco dobre dla programistów VB.NET, którzy nie chcą importować dodatkowych bibliotek tylko dla jednej funkcji. Bez ASync CTP VB.NET nie obsługuje yieldinstrukcji, więc przesyłanie strumieniowe nie jest technicznie możliwe. Dziękuję za odpowiedź. Użyję go podczas kodowania w C #. ;-)
Alex Essilfie,

2
@BenGripka: To nie to samo. Daje tylko identyfikatory klientów. Chcę całego klienta :)
ryanman

496

Wygląda mi na to, że chcesz DistinctByod MoreLINQ . Następnie możesz napisać:

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

Oto skrócona wersja DistinctBy(bez sprawdzania nieważności i bez opcji określania własnego modułu porównującego klucze):

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
Wiedziałem, że najlepszą odpowiedź opublikuje Jon Skeet, po prostu czytając tytuł postu. Jeśli ma to coś wspólnego z LINQ, Skeet jest twoim mężczyzną. Przeczytaj „C # In Depth”, aby uzyskać boską wiedzę na temat linq.
nocarrier

2
świetna odpowiedź!!! Ponadto, dla wszystkich VB_Claudlerów o yield+ dodatkowej lib, foreach można ponownie napisać jakoreturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

5
@ sudhAnsu63 jest to ograniczenie LinqToSql (i innych dostawców linq). Zamiarem LinqToX jest przetłumaczenie wyrażenia lambda C # na natywny kontekst X. Oznacza to, że LinqToSql konwertuje C # na SQL i wykonuje to polecenie natywnie, gdzie to możliwe. Oznacza to, że żadna metoda rezydująca w języku C # nie może zostać „przekazana” przez linqProvider, jeśli nie ma możliwości wyrażenia go w SQL (lub jakimkolwiek innym dostawcy linq, którego używasz). Widzę to w metodach rozszerzenia do konwersji obiektów danych do wyświetlania modeli. Można obejść ten problem, „materializując” zapytanie, wywołując ToList () przed DistinctBy ().
Michael Blackburn

1
I ilekroć wracam do tego pytania, zastanawiam się, dlaczego nie przyjmą przynajmniej części MoreLinq do BCL.
Shimmy Weitzhandler

2
@Shimmy: Z pewnością byłbym zadowolony, że ... Nie jestem pewien, co to jest wykonalność. Mogę jednak podnieść go w .NET Foundation ...
Jon Skeet

39

Podsumowując . Myślę, że większość ludzi, którzy tu przybyli, jak ja, chce najprostszego możliwego rozwiązania bez użycia bibliotek i najlepszej możliwej wydajności .

(Uważam, że grupa zaakceptowana metodą według mnie jest przesadą pod względem wydajności.)

Oto prosta metoda rozszerzenia wykorzystująca interfejs IEqualityComparer , który działa również dla wartości null.

Stosowanie:

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

Kod metody rozszerzenia

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

Nie, nie ma takiego przeciążenia metody rozszerzenia. To mnie frustrowało w przeszłości i jako takie zwykle piszę klasę pomocniczą, aby poradzić sobie z tym problemem. Celem jest konwersja Func<T,T,bool>na IEqualityComparer<T,T>.

Przykład

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

To pozwala napisać następujące

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

8
Ma to jednak nieprzyjemną implementację kodu skrótu. Łatwiej jest stworzyć IEqualityComparer<T>z projekcji: stackoverflow.com/questions/188120/…
Jon Skeet

7
(Aby wyjaśnić mój komentarz na temat kodu skrótu - bardzo łatwo jest z tym kodem skończyć na Equals (x, y) == true, ale GetHashCode (x)! = GetHashCode (y). Zasadniczo łamie to wszystko jak skrót .)
Jon Skeet,

Zgadzam się z zastrzeżeniem kodu skrótu. Mimo to +1 za wzór.
Tor Haugen,

@Jon, tak, zgadzam się, że oryginalna implementacja GetHashcode jest mniej niż optymalna (była leniwa). Zmieniłem go, aby zasadniczo używać teraz EqualityComparer <T> .Default.GetHashcode (), który jest nieco bardziej standardowy. Prawdę mówiąc, jedyną gwarancją działania implementacji GetHashcode w tym scenariuszu jest po prostu zwrócenie stałej wartości. Zabija wyszukiwanie hashtable, ale z pewnością jest poprawne funkcjonalnie.
JaredPar

1
@JaredPar: Dokładnie. Kod skrótu musi być spójny z funkcją równości, której używasz, która prawdopodobnie nie jest domyślną, inaczej nie będziesz się tym przejmować :) Dlatego wolę używać projekcji - możesz uzyskać zarówno równość, jak i rozsądny skrót kod w ten sposób. Powoduje to również, że kod wywołujący ma mniej powielania. Wprawdzie działa to tylko w przypadkach, w których chcesz dwukrotnie tę samą projekcję, ale to każdy przypadek, który widziałem w praktyce :)
Jon Skeet

18

Skrócone rozwiązanie

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

1
Czy możesz dodać wyjaśnienie, dlaczego to się poprawiło?
Keith Pinson,

To faktycznie działało dla mnie ładnie, kiedy Konrada nie.
neoscribe

13

To zrobi, co chcesz, ale nie wiem o wydajności:

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

Przynajmniej to nie jest pełne.


12

Oto prosta metoda rozszerzenia, która robi to, czego potrzebuję ...

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

Szkoda, że ​​nie upiekli takiej metody w ramach, ale hej, ho.


jest to najlepsze rozwiązanie bez konieczności dodawania tej biblioteki morelinq.
toddmo

Ale musiałem zmienić x.Keysię x.First()i zmienić wartość powrotną doIEnumerable<T>
toddmo

@toddmo Dzięki za opinię :-) Tak, brzmi logicznie ... Zaktualizuję odpowiedź po dalszym badaniu.
David Kirkland

1
nigdy nie jest za późno, aby podziękować za rozwiązanie, proste i czyste
Ali

4

Coś, co wykorzystałem, które działało dobrze dla mnie.

/// <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);
    }
}

@Musus Nie jestem pewien, dlaczego pytasz o nazwę klasy tutaj. Musiałem nazwać klasę coś, aby zaimplementować IEqualityComparer, więc po prostu poprzedziłem My.
Kleinux

4

Wszystkie rozwiązania, które tu widziałem, polegają na wyborze już porównywalnego pola. Jeśli jednak trzeba porównać inaczej, wydaje się , że to rozwiązanie działa ogólnie w przypadku czegoś takiego:

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

Co to jest LambdaComparer, skąd go importujesz?
Patrick Graham,

@PatrickGraham link w odpowiedzi: brendan.enrick.com/post/…
Dmitry Ledentsov

3

Wybierz inny sposób:

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

Sekwencja zwraca różne elementy, które porównują je według właściwości „_myCaustomerProperty”.


1
Przybyłem tutaj, aby to powiedzieć. TO powinna być zaakceptowana odpowiedź
Still.Tony

5
Nie, to nie powinna być zaakceptowana odpowiedź, chyba że wszystko, czego potrzebujesz, to odrębne wartości właściwości niestandardowej. Ogólne pytanie OP dotyczyło sposobu zwracania odrębnych obiektów na podstawie określonej właściwości obiektu.
tomo

2

Możesz użyć 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);
    }
}

Przykład użycia :

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

Źródło: https://stackoverflow.com/a/5969691/206730
Korzystanie z IEqualityComparer dla Union
Czy mogę podać własny komparator typu jawnego?


2

Możesz użyć 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

Trudnym sposobem na to jest użycie Aggregate()rozszerzenia, używając słownika jako akumulatora z wartościami klucz-właściwość jako kluczami:

var customers = new List<Customer>();

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

A rozwiązanie w stylu GroupBy wykorzystuje ToLookup():

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

Fajnie, ale dlaczego nie stworzyć Dictionary<int, Customer>zamiast tego?
ruffin

0

Zakładam, że masz IEnumerable, aw swoim przykładowym delegacie chciałbyś, aby c1 i c2 odnosiły się do dwóch elementów na tej liście?

Wierzę, że można to osiągnąć za pomocą samodzielnego łączenia var differentResults = from c1 in myList join c2 in myList on


0

Jeśli Distinct()nie da unikalnych wyników, spróbuj tego:

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

Pakiet Microsoft System.Interactive zawiera wersję Distinct, która przyjmuje klucz lambda selektora kluczy. Jest to faktycznie to samo, co rozwiązanie Jona Skeeta, ale może być pomocne dla ludzi, aby wiedzieć i sprawdzić resztę biblioteki.


0

Oto jak możesz to zrobić:

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

Ta metoda pozwala na użycie go poprzez określenie jednego parametru .MyDistinct(d => d.Name), ale pozwala również określić warunek posiadania jako drugi parametr, taki jak:

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

Uwaga: Pozwoli to również na określenie innych funkcji, takich jak na przykład .LastOrDefault(...).


Jeśli chcesz ujawnić tylko warunek, możesz go uprościć, implementując go jako:

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

W takim przypadku zapytanie wyglądałoby tak:

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

Uwaga: Wyrażenie jest tutaj prostsze, ale nuta .MyDistinct2używa .FirstOrDefault(...)domyślnie.


Uwaga: powyższe przykłady wykorzystują następującą klasę demonstracyjną

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 rozszerzenie lambda:

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

Stosowanie:

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()); 
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.