Distinct () mit Lambda?


746

Richtig, ich habe also eine Aufzählung und möchte unterschiedliche Werte daraus ziehen.

Mit System.Linqgibt es natürlich eine Erweiterungsmethode namens Distinct. Im einfachen Fall kann es ohne Parameter verwendet werden, wie zum Beispiel:

var distinctValues = myStringList.Distinct();

Schön und gut, aber wenn ich eine Aufzählung von Objekten habe, für die ich Gleichheit angeben muss, ist die einzige verfügbare Überladung:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Das Gleichheitsvergleichsargument muss eine Instanz von sein IEqualityComparer<T>. Ich kann das natürlich, aber es ist etwas ausführlich und ungeschickt.

Was ich erwartet hätte, ist eine Überlastung, die ein Lambda benötigt, sagen wir eine Func <T, T, bool>:

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

Weiß jemand, ob es eine solche Erweiterung oder eine gleichwertige Problemumgehung gibt? Oder fehlt mir etwas?

Gibt es alternativ eine Möglichkeit, einen IEqualityComparer inline anzugeben (peinlich für mich)?

Aktualisieren

Ich habe eine Antwort von Anders Hejlsberg auf einen Beitrag in einem MSDN-Forum zu diesem Thema gefunden. Er sagt:

Das Problem, auf das Sie stoßen werden, ist, dass zwei Objekte, wenn sie gleich sind, denselben GetHashCode-Rückgabewert haben müssen (andernfalls funktioniert die von Distinct intern verwendete Hash-Tabelle nicht richtig). Wir verwenden IEqualityComparer, weil es kompatible Implementierungen von Equals und GetHashCode in einer einzigen Schnittstelle verpackt.

Ich nehme an, das macht Sinn ..


2
siehe stackoverflow.com/questions/1183403/... für eine Lösung mit GroupBy

17
Danke für das Anders Hejlsberg Update!
Tor Haugen

Nein, es macht keinen Sinn - wie würden zwei Objekte, die identische Werte enthalten, zwei verschiedene Hash-Codes zurückgeben?
GY

Es könnte helfen - eine Lösung für .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId))und erklären, warum GetHashCode () wichtig ist, um richtig zu funktionieren.
Marbel82

In Verbindung stehendes / mögliches Duplikat von: LINQs Distinct () auf einer bestimmten Eigenschaft
Marc.2377

Antworten:


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

12
Ausgezeichnet! Dies ist wirklich einfach in eine Erweiterungsmethode zu kapseln, wie DistinctBy(oder sogar Distinct, da die Signatur eindeutig sein wird).
Tomas Aschan

1
Funktioniert bei mir nicht! <Die Methode 'First' kann nur als letzte Abfrageoperation verwendet werden. Verwenden Sie stattdessen in diesem Fall die Methode 'FirstOrDefault'.> Selbst wenn ich 'FirstOrDefault' ausprobiert habe, hat dies nicht funktioniert.
JatSing

63
@TorHaugen: Seien Sie sich nur bewusst, dass die Erstellung all dieser Gruppen mit Kosten verbunden ist. Dies kann die Eingabe nicht streamen und puffert am Ende alle Daten, bevor etwas zurückgegeben wird. Das mag natürlich nicht relevant für Ihre Situation sein, aber ich bevorzuge die Eleganz von DistinctBy :)
Jon Skeet

2
@ JonSkeet: Dies ist gut genug für VB.NET-Codierer, die keine zusätzlichen Bibliotheken für nur eine Funktion importieren möchten. Ohne ASync CTP unterstützt VB.NET die yieldAnweisung nicht, sodass Streaming technisch nicht möglich ist. Vielen Dank für Ihre Antwort. Ich werde es beim Codieren in C # verwenden. ;-)
Alex Essilfie

2
@ BenGripka: Das ist nicht ganz dasselbe. Es gibt Ihnen nur die Kunden-IDs. Ich will den ganzen Kunden :)
Ryanman

496

Es sieht für mich so DistinctByaus, als ob Sie von MoreLINQ wollen . Sie können dann schreiben:

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

Hier ist eine DistinctByabgespeckte Version von (keine Überprüfung der Nichtigkeit und keine Option zum Angeben eines eigenen Schlüsselvergleichs):

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
Ich wusste, dass Jon Skeet die beste Antwort einfach durch Lesen des Titels des Beitrags veröffentlichen würde. Wenn es etwas mit LINQ zu tun hat, ist Skeet dein Mann. Lesen Sie 'C # In Depth', um gottähnliches Linq-Wissen zu erlangen.
Nocarrier

2
gute Antwort!!! Außerdem yieldkann foreach für alle VB_Complainer über die + extra lib alsreturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

5
@ sudhAnsu63 Dies ist eine Einschränkung von LinqToSql (und anderen Linq-Anbietern). Die Absicht von LinqToX ist es, Ihren C # -Lambda-Ausdruck in den nativen Kontext von X zu übersetzen. Das heißt, LinqToSql konvertiert Ihr C # in SQL und führt diesen Befehl nach Möglichkeit nativ aus. Dies bedeutet, dass eine Methode, die sich in C # befindet, nicht durch einen linqProvider "übergeben" werden kann, wenn es keine Möglichkeit gibt, sie in SQL (oder einem anderen von Ihnen verwendeten Linq-Anbieter) auszudrücken. Ich sehe dies in Erweiterungsmethoden zum Konvertieren von Datenobjekten in Ansichtsmodelle. Sie können dies umgehen, indem Sie die Abfrage "materialisieren" und ToList () vor DistinctBy () aufrufen.
Michael Blackburn

1
Und wenn ich auf diese Frage zurückkomme, frage ich mich immer wieder, warum sie nicht zumindest einen Teil von MoreLinq in die BCL aufnehmen.
Shimmy Weitzhandler

2
@ Shimmy: Ich würde das auf jeden Fall begrüßen ... Ich bin mir nicht sicher, was die Machbarkeit ist. Ich kann es aber in der .NET Foundation erheben ...
Jon Skeet

39

Dinge einpacken . Ich denke, die meisten Leute, die wie ich hierher gekommen sind, wollen die einfachste Lösung, die möglich ist, ohne Bibliotheken zu verwenden und mit der bestmöglichen Leistung .

(Die akzeptierte Gruppe nach Methode ist für mich meiner Meinung nach ein Overkill in Bezug auf die Leistung.)

Hier ist eine einfache Erweiterungsmethode, die die IEqualityComparer- Schnittstelle verwendet und auch für Nullwerte funktioniert.

Verwendungszweck:

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

Code der Erweiterungsmethode

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

Nein, dafür gibt es keine solche Überlastung der Erweiterungsmethode. Ich fand das in der Vergangenheit selbst frustrierend und als solches schreibe ich normalerweise eine Hilfsklasse, um dieses Problem zu lösen. Das Ziel ist es, ein Func<T,T,bool>in umzuwandeln IEqualityComparer<T,T>.

Beispiel

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

Auf diese Weise können Sie Folgendes schreiben

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

8
Das hat allerdings eine böse Implementierung von Hash-Code. Es ist einfacher, eine IEqualityComparer<T>aus einer Projektion zu erstellen : stackoverflow.com/questions/188120/…
Jon Skeet

7
(Nur um meinen Kommentar zum Hash-Code zu erklären - mit diesem Code ist es sehr einfach, Equals (x, y) == true zu erhalten, aber GetHashCode (x)! = GetHashCode (y). Das bricht im Grunde alles wie eine Hashtabelle .)
Jon Skeet

Ich stimme dem Einwand des Hash-Codes zu. Trotzdem +1 für das Muster.
Tor Haugen

@ Jon, ja, ich stimme zu, dass die ursprüngliche Implementierung von GetHashcode nicht optimal ist (war faul). Ich habe es so umgestellt, dass es jetzt im Wesentlichen EqualityComparer <T> .Default.GetHashcode () verwendet, was etwas mehr Standard ist. Um ehrlich zu sein, besteht die einzige Garantie für die GetHashcode-Implementierung in diesem Szenario darin, einfach einen konstanten Wert zurückzugeben. Tötet die Suche nach Hashtabellen, ist jedoch garantiert funktional korrekt.
JaredPar

1
@ JaredPar: Genau. Der Hash-Code muss mit der von Ihnen verwendeten Gleichheitsfunktion übereinstimmen, die vermutlich nicht die Standardfunktion ist, sonst würden Sie sich nicht darum kümmern :) Deshalb bevorzuge ich die Verwendung einer Projektion - Sie können sowohl Gleichheit als auch einen vernünftigen Hash erhalten Code auf diese Weise. Außerdem wird der aufrufende Code weniger dupliziert. Zugegeben, es funktioniert nur in Fällen, in denen Sie zweimal dieselbe Projektion wünschen, aber das ist jeder Fall, den ich in der Praxis gesehen habe :)
Jon Skeet

18

Kurzlösung

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

1
Können Sie eine Erklärung hinzufügen, warum dies verbessert wurde?
Keith Pinson

Das hat bei mir wirklich gut funktioniert, als Konrad es nicht tat.
neoscribe

13

Dies wird tun, was Sie wollen, aber ich weiß nicht über die Leistung:

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

Zumindest ist es nicht ausführlich.


12

Hier ist eine einfache Erweiterungsmethode, die das tut, was ich brauche ...

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

Es ist eine Schande, dass sie keine bestimmte Methode wie diese in das Framework gebacken haben, aber hey ho.


Dies ist die beste Lösung, ohne dass diese Bibliothek morelinq hinzugefügt werden muss.
Toddmo

Aber musste ich ändern x.Keyzu x.First()und den Rückgabewert zu ändernIEnumerable<T>
toddmo

@toddmo Danke für das Feedback :-) Ja, klingt logisch ... Ich werde die Antwort aktualisieren, nachdem ich weitere Nachforschungen angestellt habe.
David Kirkland

1
Es ist nie zu spät, sich für die einfache und saubere Lösung zu bedanken
Ali

4

Etwas, das ich benutzt habe und das für mich gut funktioniert hat.

/// <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 Ich bin mir nicht sicher, warum du hier nach dem Klassennamen fragst. Ich musste der Klasse einen Namen geben, um IEqualityComparer zu implementieren, also habe ich nur das My vorangestellt.
Kleinux

4

Alle Lösungen, die ich hier gesehen habe, beruhen auf der Auswahl eines bereits vergleichbaren Bereichs. Wenn man jedoch anders vergleichen muss, scheint diese Lösung hier allgemein zu funktionieren, für etwas wie:

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

Was ist LambdaComparer, woher importieren Sie das?
Patrick Graham

@PatrickGraham in der Antwort verlinkt
Dmitry Ledentsov

3

Nehmen Sie einen anderen Weg:

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

Die Sequenz gibt unterschiedliche Elemente zurück, die sie mit der Eigenschaft '_myCaustomerProperty' vergleichen.


1
Kam hierher, um das zu sagen. Dies sollte die akzeptierte Antwort sein
Still.Tony

5
Nein, dies sollte nicht die akzeptierte Antwort sein, es sei denn, Sie möchten nur unterschiedliche Werte der benutzerdefinierten Eigenschaft. Die allgemeine OP-Frage lautete, wie unterschiedliche Objekte basierend auf einer bestimmten Eigenschaft des Objekts zurückgegeben werden können.
Tomo

2

Sie können InlineComparer verwenden

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

Anwendungsbeispiel :

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

Quelle: https://stackoverflow.com/a/5969691/206730
Verwenden von IEqualityComparer für Union Kann ich meinen expliziten Typkomparator
inline angeben?


2

Sie können LambdaEqualityComparer verwenden:

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

Eine schwierige Möglichkeit hierfür ist die Verwendung der Aggregate()Erweiterung, bei der ein Wörterbuch als Akkumulator mit den Schlüsseleigenschaftswerten als Schlüssel verwendet wird:

var customers = new List<Customer>();

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

Und eine GroupBy-ähnliche Lösung verwendet ToLookup():

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

Schön, aber warum nicht einfach eine erstellen Dictionary<int, Customer>?
Ruffin

0

Ich gehe davon aus, dass Sie eine IEnumerable haben und in Ihrem Beispieldelegierten möchten Sie, dass c1 und c2 auf zwei Elemente in dieser Liste verweisen?

Ich glaube, Sie könnten dies mit einem Self-Join erreichen. Var differentResults = from c1 in myList join c2 in myList on


0

Wenn Sie Distinct()keine eindeutigen Ergebnisse erzielen, versuchen Sie Folgendes:

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

Das Microsoft System.Interactive-Paket enthält eine Version von Distinct, die ein Lambda für die Schlüsselauswahl verwendet. Dies ist praktisch die gleiche Lösung wie die von Jon Skeet, kann jedoch hilfreich sein, damit die Benutzer sie kennen und den Rest der Bibliothek überprüfen können.


0

So geht's:

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

Mit dieser Methode können Sie sie verwenden, indem Sie einen Parameter wie .MyDistinct(d => d.Name)angeben, aber Sie können auch eine Bedingung mit einem zweiten Parameter wie folgt angeben:

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

Hinweis: Auf diese Weise können Sie auch andere Funktionen wie z. B. angeben .LastOrDefault(...).


Wenn Sie nur die Bedingung verfügbar machen möchten, können Sie sie noch einfacher gestalten, indem Sie sie wie folgt implementieren:

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

In diesem Fall würde die Abfrage nur so aussehen:

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

NB Hier ist der Ausdruck einfacher, aber Note .MyDistinct2verwendet .FirstOrDefault(...)implizit.


Hinweis: In den obigen Beispielen wird die folgende Demo-Klasse verwendet

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 Lambda-Erweiterung:

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

Verwendungszweck:

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()); 
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.