LINQs Distinct () für eine bestimmte Eigenschaft


1095

Ich spiele mit LINQ, um mehr darüber zu erfahren, aber ich kann nicht herausfinden, wie Distinctich es verwenden soll, wenn ich keine einfache Liste habe (eine einfache Liste von ganzen Zahlen ist ziemlich einfach, das ist nicht die Frage). Was kann ich tun, wenn Distinct für eine Liste eines Objekts für eine oder mehrere Eigenschaften des Objekts verwendet werden soll?

Beispiel: Wenn ein Objekt ist Person, mit Eigenschaft Id. Wie kann ich alle Personen abrufen und Distinctmit der Eigenschaft Iddes Objekts verwenden?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Wie kann ich nur Person1und bekommen Person3? Ist das möglich?

Wenn dies mit LINQ nicht möglich ist, wie lässt sich eine Liste in PersonAbhängigkeit von einigen Eigenschaften in .NET 3.5 am besten erstellen ?

Antworten:


1247

EDIT : Dies ist jetzt Teil von MoreLINQ .

Was Sie brauchen, ist ein "Unterscheidungsmerkmal" effektiv. Ich glaube nicht, dass es Teil von LINQ ist, obwohl es ziemlich einfach zu schreiben ist:

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

Um die unterschiedlichen Werte nur anhand der IdEigenschaft zu ermitteln, können Sie Folgendes verwenden:

var query = people.DistinctBy(p => p.Id);

Um mehrere Eigenschaften zu verwenden, können Sie anonyme Typen verwenden, die die Gleichheit entsprechend implementieren:

var query = people.DistinctBy(p => new { p.Id, p.Name });

Ungetestet, aber es sollte funktionieren (und es wird jetzt zumindest kompiliert).

Es wird jedoch der Standardkomparator für die Schlüssel angenommen. Wenn Sie einen Gleichheitsvergleich übergeben möchten, geben Sie ihn einfach an den HashSetKonstruktor weiter.



1
@ ashes999: Ich bin nicht sicher, was du meinst. Der Code ist in der Antwort und in der Bibliothek vorhanden - je nachdem, ob Sie gerne eine Abhängigkeit annehmen.
Jon Skeet

10
@ ashes999: Wenn Sie dies nur an einem einzigen Ort tun, ist die Verwendung sicher GroupByeinfacher. Wenn Sie es an mehr als einem Ort benötigen, ist es viel sauberer (IMO), die Absicht zusammenzufassen.
Jon Skeet

5
@MatthewWhited: Da hier nichts erwähnt wird IQueryable<T>, sehe ich nicht, wie relevant es ist. Ich bin damit einverstanden, dass dies nicht für EF usw. geeignet wäre, aber innerhalb von LINQ to Objects denke ich, dass es besser geeignet ist als GroupBy. Der Kontext der Frage ist immer wichtig.
Jon Skeet

7
Das Projekt wurde auf Github fortgesetzt. Hier ist der Code von DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01

1858

Was ist, wenn ich eine eindeutige Liste basierend auf einer oder mehreren Eigenschaften erhalten möchte ?

Einfach! Sie möchten sie gruppieren und einen Gewinner aus der Gruppe auswählen.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Wenn Sie Gruppen für mehrere Eigenschaften definieren möchten, gehen Sie wie folgt vor:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();

1
@ErenErsonmez sicher. Wenn mit meinem gebuchten Code eine verzögerte Ausführung gewünscht wird, lassen Sie den ToList-Aufruf aus.
Amy B

5
Sehr schöne Antwort! Ich habe mir wirklich geholfen, Linq-to-Entities aus einer SQL-Ansicht zu erstellen, in der ich die Ansicht nicht ändern konnte. Ich musste FirstOrDefault () anstelle von First () verwenden - alles ist gut.
Alex KeySmith

8
Ich habe es versucht und es sollte sich in Select (g => g.FirstOrDefault ())

26
@ChocapicSz Nein. Beide Single()und SingleOrDefault()jeder Wurf, wenn die Quelle mehr als einen Gegenstand hat. Bei dieser Operation erwarten wir die Möglichkeit, dass jede Gruppe mehr als ein Element hat. In diesem Fall First()wird es vorgezogen, FirstOrDefault()weil jede Gruppe mindestens ein Mitglied haben muss ... es sei denn, Sie verwenden EntityFramework, das nicht herausfinden kann, dass jede Gruppe mindestens ein Mitglied hat und Anforderungen stellt FirstOrDefault().
Amy B

2
Scheint derzeit in EF Core nicht unterstützt zu werden, selbst wenn ich FirstOrDefault() github.com/dotnet/efcore/issues/12088 verwende. Ich bin auf 3.1 und bekomme Fehler, dass ich nicht übersetzen kann.
Collin M. Barrett

78

Verwenden:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

Das wherehilft Ihnen , die Einträge zu filtern (könnte komplexer sein) und die groupbyund selectführen die unterschiedliche Funktion.


1
Perfekt und funktioniert ohne Erweiterung von Linq oder Verwendung einer anderen Abhängigkeit.
DavidScherer

77

Sie können auch die Abfragesyntax verwenden, wenn Sie möchten, dass sie alle LINQ-ähnlich aussieht:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();

4
Hmm, meine Gedanken sind, dass sowohl die Abfragesyntax als auch die fließende API-Syntax genauso LINQ sind wie die anderen und dass sie nur bevorzugt werden, welche verwendet werden. Ich selbst bevorzuge die fließende API, also würde ich das als LINK-ähnlicher betrachten, aber dann denke ich, dass das subjektiv ist
Max Carroll

LINQ-Like hat nichts mit Präferenz zu tun. "LINQ-Like" hat damit zu tun, dass eine andere Abfragesprache in C # eingebettet ist. Ich bevorzuge die fließende Oberfläche, die aus Java-Streams stammt, aber NICHT LINQ-Like.
Ryan The Leach

Ausgezeichnet!! Sie sind mein Held!
Farzin Kanzi

63

Ich denke es ist genug:

list.Select(s => s.MyField).Distinct();

43
Was ist, wenn er sein volles Objekt zurück braucht, nicht nur dieses bestimmte Feld?
Festim Cahani

1
Welches Objekt der verschiedenen Objekte mit demselben Eigenschaftswert?
DonRumatta

40

Lösung Gruppieren Sie zuerst nach Ihren Feldern und wählen Sie dann das erste oder Standardelement aus.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();

26

Sie können dies mit dem Standard tun Linq.ToLookup(). Dadurch wird eine Sammlung von Werten für jeden eindeutigen Schlüssel erstellt. Wählen Sie einfach den ersten Artikel in der Sammlung aus

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

Der folgende Code entspricht funktional der Antwort von Jon Skeet .

Unter .NET 4.5 getestet, sollte auf jeder früheren Version von LINQ funktionieren.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Schauen Sie sich übrigens Jon Skeets neueste Version von DistinctBy.cs in Google Code an .


3
Dies gab mir eine "Sequenz hat keinen Wertefehler", aber Skeets Antwort ergab das richtige Ergebnis.
Was wäre cool

10

Ich habe einen Artikel geschrieben, in dem erklärt wird, wie die Distinct-Funktion erweitert wird, sodass Sie wie folgt vorgehen können:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Hier ist der Artikel: Erweitern von LINQ - Angeben einer Eigenschaft in der Distinct-Funktion


3
Ihr Artikel hat einen Fehler, es sollte ein <T> nach Distinct: public static IEnumerable <T> Distinct (dies ... Es sieht auch nicht so aus, als würde es (gut) für mehr als eine Eigenschaft funktionieren, dh eine Kombination aus first und Nachnamen.
Reihe1

2
+1, ein kleiner Fehler ist kein Grund genug für eine Abwertung, die einfach so albern ist und oft einen Tippfehler genannt wird. Und ich muss noch eine generische Funktion sehen, die für eine beliebige Anzahl von Immobilien funktioniert! Ich hoffe, der Downvoter hat auch jede andere Antwort in diesem Thread abgelehnt. Aber hey, was ist dieser zweite Typ als Objekt? Ich erhebe Einspruch !
Nawfal

4
Ihr Link ist kaputt
Tom Lint

7

Persönlich benutze ich die folgende Klasse:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Dann eine Erweiterungsmethode:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Schließlich die beabsichtigte Verwendung:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

Der Vorteil, den ich bei diesem Ansatz gefunden habe, ist die Wiederverwendung von LambdaEqualityComparer Klasse für andere Methoden, die eine akzeptieren IEqualityComparer. (Oh, und ich überlasse das yieldZeug der ursprünglichen LINQ-Implementierung ...)


5

Falls Sie eine Distinct-Methode für mehrere Eigenschaften benötigen, können Sie meine PowerfulExtensions überprüfen Bibliothek . Derzeit befindet es sich in einem sehr jungen Stadium, aber Sie können bereits Methoden wie Distinct, Union, Intersect, außer für eine beliebige Anzahl von Eigenschaften verwenden.

So verwenden Sie es:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

5

Als wir in unserem Projekt vor einer solchen Aufgabe standen, haben wir eine kleine API definiert, um Komparatoren zu erstellen.

Der Anwendungsfall war also wie folgt:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

Und die API selbst sieht folgendermaßen aus:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Weitere Details finden Sie auf unserer Website: IEqualityComparer in LINQ .


5

Mit DistinctBy () können Sie Distinct-Datensätze anhand einer Objekteigenschaft abrufen. Fügen Sie einfach die folgende Anweisung hinzu, bevor Sie sie verwenden:

Verwenden von Microsoft.Ajax.Utilities;

und verwenden Sie es dann wie folgt:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

Dabei ist 'Index' die Eigenschaft, für die die Daten unterschiedlich sein sollen.


4

Sie können es so machen (wenn auch nicht blitzschnell):

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

Das heißt: "Wählen Sie alle Personen aus, bei denen die Liste keine andere Person mit derselben ID enthält."

Wohlgemerkt, in Ihrem Beispiel würde das nur Person 3 auswählen. Ich bin mir nicht sicher, wie ich aus den beiden vorherigen sagen soll, was Sie wollen.


4

Wenn Sie die MoreLinq-Bibliothek nicht zu Ihrem Projekt hinzufügen möchten, um die DistinctByFunktionalität zu erhalten , können Sie das gleiche Endergebnis erzielen, indem Sie die Überladung der Linq- DistinctMethode verwenden, die ein IEqualityComparerArgument enthält.

Sie erstellen zunächst eine generische benutzerdefinierte Gleichheitsvergleichsklasse, die die Lambda-Syntax verwendet, um einen benutzerdefinierten Vergleich zweier Instanzen einer generischen Klasse durchzuführen:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

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

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

Dann verwenden Sie es in Ihrem Hauptcode wie folgt:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Voila! :) :)

Das Obige setzt Folgendes voraus:

  • Eigentum Person.Idist vom Typint
  • Die peopleSammlung enthält keine Nullelemente

Wenn die Sammlung Nullen enthalten könnte, schreiben Sie die Lambdas einfach neu, um nach Null zu suchen, z.

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

BEARBEITEN

Dieser Ansatz ähnelt dem in der Antwort von Vladimir Nesterovsky, ist jedoch einfacher.

Es ähnelt auch dem in Joels Antwort, ermöglicht jedoch eine komplexe Vergleichslogik mit mehreren Eigenschaften.

Wenn sich Ihre Objekte jedoch nur bis dahin unterscheiden können, Idhat ein anderer Benutzer die richtige Antwort gegeben. Sie müssen lediglich die Standardimplementierungen von GetHashCode()und Equals()in Ihrer PersonKlasse überschreiben und dann einfach die Out-of-the-Box- Distinct()Methode von Linq zum Filtern verwenden Duplikate aus.


Ich möchte nur eindeutige Elemente in diktonischer Form erhalten. Können Sie mir bitte helfen? Ich verwende diesen Code. Wenn TempDT nichts ist, dann m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Funktion (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Funktion (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB


1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();

Wolltest du Select() new Personstatt new Player? Die Tatsache, dass Sie bei bestellen, IDinformiert Sie jedoch nicht darüber Distinct(), diese Eigenschaft zur Bestimmung der Eindeutigkeit zu verwenden, sodass dies nicht funktioniert.
Speck

1

Überschreibung equals (Objekt obj) und GetHashCode () Methode:

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

und dann einfach anrufen:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

Allerdings sollte GetHashCode () weiter fortgeschritten sein (um auch den Namen zu zählen), diese Antwort ist meiner Meinung nach wahrscheinlich die beste. Um die Ziellogik zu archivieren, muss GetHashCode () nicht überschrieben werden. Equals () reicht aus. Wenn wir jedoch Leistung benötigen, müssen wir sie überschreiben. Alle Vergleichsalgen überprüfen zuerst den Hash, und wenn sie gleich sind, rufen Sie Equals () auf.
Oleg Skripnyak

Außerdem sollte dort in Equals () die erste Zeile "if (! (Obj is Person)) return false" sein. Es wird jedoch empfohlen, ein separates Objekt zu verwenden, das in einen Typ umgewandelt wurde, z. B. "var o = obj als Person; if (o == null) return false;" dann überprüfen Sie die Gleichheit mit o ohne Casting
Oleg Skripnyak

1
Das Überschreiben von Gleichen wie diesen ist keine gute Idee, da dies unbeabsichtigte Konsequenzen für andere Programmierer haben könnte, die erwarten, dass die Gleichheit der Person für mehr als eine einzelne Eigenschaft bestimmt wird.
B2K

0

Sie sollten in der Lage sein, Equals on person zu überschreiben, um Equals on Person.id tatsächlich auszuführen. Dies sollte zu dem Verhalten führen, nach dem Sie suchen.


-5

Bitte versuchen Sie es mit dem folgenden Code.

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
Eine kurze Antwort ist willkommen, bietet jedoch den letzteren Benutzern, die versuchen zu verstehen, was hinter dem Problem steckt, nicht viel Wert. Bitte nehmen Sie sich etwas Zeit, um zu erklären, was das eigentliche Problem ist und wie es gelöst werden kann. Vielen Dank ~
Gehört
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.