LINQ's Distinct () sur une propriété particulière

1095

Je joue avec LINQ pour en savoir plus, mais je ne sais pas comment l'utiliser Distinctquand je n'ai pas de liste simple (une simple liste d'entiers est assez facile à faire, ce n'est pas la question). Que dois-je faire si je veux utiliser Distinct sur une liste d'objets sur une ou plusieurs propriétés de l'objet?

Exemple: Si un objet l'est Person, avec Propriété Id. Comment puis-je obtenir toutes les personnes et les utiliser Distinctavec la propriété Idde l'objet?

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

Comment puis-je obtenir juste Person1et Person3? Est-ce possible?

Si ce n'est pas possible avec LINQ, quelle serait la meilleure façon d'avoir une liste en Personfonction de certaines de ses propriétés dans .NET 3.5?

Patrick Desjardins
la source

Réponses:

1249

EDIT : Cela fait maintenant partie de MoreLINQ .

Ce dont vous avez besoin, c'est d'un "distinct par" efficace. Je ne pense pas que cela fasse partie de LINQ tel quel, bien qu'il soit assez facile d'écrire:

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

Donc, pour trouver les valeurs distinctes en utilisant uniquement la Idpropriété, vous pouvez utiliser:

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

Et pour utiliser plusieurs propriétés, vous pouvez utiliser des types anonymes, qui implémentent correctement l'égalité:

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

Non testé, mais cela devrait fonctionner (et maintenant il compile au moins).

Il suppose que le comparateur par défaut pour les clés - si vous voulez passer dans un comparateur d'égalité, il suffit de le transmettre au HashSetconstructeur.

Jon Skeet
la source
1
@ ashes999: Je ne sais pas ce que tu veux dire. Le code est présent dans la réponse et dans la bibliothèque - selon que vous êtes satisfait ou non d'une dépendance.
Jon Skeet
10
@ ashes999: Si vous ne faites cela qu'au même endroit, alors, bien sûr, l'utilisation GroupByest plus simple. Si vous en avez besoin à plus d'un endroit, il est beaucoup plus propre (OMI) d'encapsuler l'intention.
Jon Skeet
5
@MatthewWhited: Étant donné qu'il n'y a aucune mention IQueryable<T>ici, je ne vois pas en quoi c'est pertinent. Je suis d'accord que cela ne conviendrait pas pour EF, etc., mais dans LINQ to Objects, je pense que c'est plus approprié que GroupBy. Le contexte de la question est toujours important.
Jon Skeet
7
Le projet a évolué sur github, voici le code de DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01
1859

Que faire si je souhaite obtenir une liste distincte basée sur une ou plusieurs propriétés?

Facile! Vous voulez les regrouper et choisir un gagnant dans le groupe.

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

Si vous souhaitez définir des groupes sur plusieurs propriétés, voici comment:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();
Amy B
la source
1
@ErenErsonmez bien sûr. Avec mon code publié, si une exécution différée est souhaitée, quittez l'appel ToList.
Amy B
5
Très belle réponse! Vraiment m'a aidé dans Linq-to-Entities piloté à partir d'une vue SQL où je ne pouvais pas modifier la vue. J'avais besoin d'utiliser FirstOrDefault () plutôt que First () - tout va bien.
Alex KeySmith
8
Je l'ai essayé et il devrait changer pour Select (g => g.FirstOrDefault ())
26
@ChocapicSz Nope. Les deux Single()et SingleOrDefault()chaque lancer lorsque la source a plus d'un élément. Dans cette opération, nous nous attendons à la possibilité que chaque groupe puisse avoir plus d'un élément. D'ailleurs, First()est préférable à FirstOrDefault()parce que chaque groupe doit avoir au moins un membre .... à moins que vous n'utilisiez EntityFramework, qui ne peut pas comprendre que chaque groupe a au moins un membre et demande FirstOrDefault().
Amy B
2
Semble ne pas être actuellement pris en charge dans EF Core, même en utilisant FirstOrDefault() github.com/dotnet/efcore/issues/12088 Je suis sur 3.1 et j'obtiens des erreurs "incapable de traduire".
Collin M. Barrett
78

Utilisation:

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

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

Le wherevous aide à filtrer les entrées (pourrait être plus complexe) groupbyet à selecteffectuer la fonction distincte.

karcsi
la source
1
Parfait et fonctionne sans étendre Linq ni utiliser une autre dépendance.
DavidScherer
77

Vous pouvez également utiliser la syntaxe de requête si vous souhaitez qu'elle ressemble à LINQ:

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();
Chuck Rostance
la source
4
Hmm mes pensées sont à la fois la syntaxe de la requête et la syntaxe fluide de l'API sont tout aussi LINQ comme les uns des autres et sa juste préférence sur ceux que les gens utilisent. Je préfère moi-même l'API couramment, donc je considérerais que plus comme LINK mais je suppose que c'est subjectif
Max Carroll
LINQ-Like n'a rien à voir avec les préférences, étant "LINQ-like" a à voir avec ressembler à un langage de requête différent étant incorporé dans C #, je préfère l'interface fluide, provenant de flux java, mais ce n'est PAS LINQ-Like.
Ryan The Leach
Excellent!! Tu es mon héros!
Farzin Kanzi
63

Je pense que cela suffit:

list.Select(s => s.MyField).Distinct();
Ivan
la source
43
Et s'il avait besoin de récupérer son objet complet, pas seulement ce domaine particulier?
Festim Cahani
1
Quel objet exactement parmi plusieurs objets qui ont la même valeur de propriété?
donRumatta
40

Solution, regroupez d'abord vos champs, puis sélectionnez le premier élément par défaut.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
cahit beyaz
la source
26

Vous pouvez le faire avec la norme Linq.ToLookup(). Cela créera une collection de valeurs pour chaque clé unique. Sélectionnez simplement le premier article de la collection

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
David Fahlander
la source
17

Le code suivant est fonctionnellement équivalent à la réponse de Jon Skeet .

Testé sur .NET 4.5, devrait fonctionner sur n'importe quelle version antérieure de LINQ.

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

Soit dit en passant, consultez la dernière version de Jon Skeet de DistinctBy.cs sur Google Code .

Contango
la source
3
Cela m'a donné une "erreur de séquence sans valeurs", mais la réponse de Skeet a produit le résultat correct.
Qu'est-ce qui serait cool
10

J'ai écrit un article qui explique comment étendre la fonction Distinct afin que vous puissiez faire comme suit:

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.

Voici l'article: Extension de LINQ - Spécification d'une propriété dans la fonction distincte

Timothy Khouri
la source
3
Votre article contient une erreur, il devrait y avoir un <T> après Distinct: public static IEnumerable <T> Distinct (this ... et noms de famille
row1
2
+1, une erreur mineure n'est pas une raison suffisante pour downvote, si juste idiot, a souvent appelé une faute de frappe. Et je n'ai pas encore vu de fonction générique qui fonctionnera pour n'importe quel nombre de propriétés! J'espère que le downvoter a également downvoté toutes les autres réponses dans ce fil. Mais bon quel est ce second type étant objet ?? Je proteste !
nawfal
4
Votre lien est rompu
Tom Lint
7

Personnellement, j'utilise la classe suivante:

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

Ensuite, une méthode d'extension:

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

Enfin, l'usage prévu:

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

L'avantage que j'ai trouvé en utilisant cette approche est la réutilisation de la LambdaEqualityComparerclasse pour d'autres méthodes qui acceptent un IEqualityComparer. (Oh, et je laisse le yieldtruc à l'implémentation LINQ originale ...)

Joel
la source
5

Si vous avez besoin d'une méthode Distinct sur plusieurs propriétés, vous pouvez consulter mes PowerfulExtensions bibliothèque . Actuellement, c'est à un stade très jeune, mais vous pouvez déjà utiliser des méthodes telles que Distinct, Union, Intersect, Except sur un certain nombre de propriétés;

Voici comment vous l'utilisez:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Andrzej Gis
la source
5

Lorsque nous avons été confrontés à une telle tâche dans notre projet, nous avons défini une petite API pour composer des comparateurs.

Donc, le cas d'utilisation était comme ceci:

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

Et l'API elle-même ressemble à ceci:

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

Plus de détails sont sur notre site: IEqualityComparer dans LINQ .

Vladimir Nesterovsky
la source
5

Vous pouvez utiliser DistinctBy () pour obtenir des enregistrements Distinct par une propriété d'objet. Ajoutez simplement la déclaration suivante avant de l'utiliser:

en utilisant Microsoft.Ajax.Utilities;

puis l'utiliser comme suit:

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

où 'Index' est la propriété sur laquelle je veux que les données soient distinctes.

Harry .Naeem
la source
4

Vous pouvez le faire (mais pas très vite) comme ceci:

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

Autrement dit, "sélectionnez toutes les personnes où il n'y a pas une autre personne différente dans la liste avec le même ID."

Remarquez, dans votre exemple, que cela ne ferait que sélectionner la personne 3. Je ne sais pas comment dire ce que vous voulez, parmi les deux précédents.

mqp
la source
4

Si vous ne voulez pas ajouter la bibliothèque MoreLinq à votre projet juste pour obtenir la DistinctByfonctionnalité, vous pouvez obtenir le même résultat final en utilisant la surcharge de la Distinctméthode de Linq qui prend un IEqualityComparerargument.

Vous commencez par créer une classe de comparaison d'égalité personnalisée générique qui utilise la syntaxe lambda pour effectuer une comparaison personnalisée de deux instances d'une classe générique:

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

Ensuite, dans votre code principal, vous l'utilisez comme suit:

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! :)

Ce qui précède suppose ce qui suit:

  • La propriété Person.Idest de typeint
  • La peoplecollection ne contient aucun élément nul

Si la collection peut contenir des valeurs nulles, réécrivez simplement les lambdas pour vérifier la valeur null, par exemple:

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

ÉDITER

Cette approche est similaire à celle de la réponse de Vladimir Nesterovsky mais plus simple.

Il est également similaire à celui de la réponse de Joel mais permet une logique de comparaison complexe impliquant plusieurs propriétés.

Cependant, si vos objets ne peuvent différer que par Idun autre utilisateur, la réponse correcte est que tout ce que vous devez faire est de remplacer les implémentations par défaut de GetHashCode()et Equals()dans votre Personclasse, puis d'utiliser simplement la Distinct()méthode prête à l'emploi de Linq pour filtrer tous les doublons.

Canpien de la mer Caspienne
la source
Je souhaite obtenir uniquement des éléments uniques dans le dicton, pouvez-vous aider, j'utilise ce code si TempDT n'est pas rien alors m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB
2

La meilleure façon de le faire qui sera compatible avec d'autres versions de .NET est de remplacer Equals et GetHash pour gérer cela (voir la question Stack Overflow Ce code renvoie des valeurs distinctes. Cependant, ce que je veux, c'est renvoyer une collection fortement typée par opposition à un type anonyme ), mais si vous avez besoin de quelque chose de générique dans votre code, les solutions de cet article sont excellentes.

gcoleman0828
la source
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();
Arindam
la source
Voulez-vous dire au Select() new Personlieu de new Player? Cependant, le fait que vous passiez une commande IDn'indique pas Distinct()d'utiliser cette propriété pour déterminer l'unicité, donc cela ne fonctionnera pas.
BACON
1

Substituez les méthodes Equals (object obj) et GetHashCode () :

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

puis appelez simplement:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();
Waldemar Gałęzinowski
la source
Cependant GetHashCode () devrait être plus avancé (pour compter également le nom), cette réponse est probablement la meilleure à mon avis. En fait, pour archiver la logique cible, il n'est pas nécessaire de remplacer GetHashCode (), Equals () est suffisant, mais si nous avons besoin de performances, nous devons le remplacer. Toutes les algues de comparaison, vérifiez d'abord le hachage, et si elles sont égales, appelez Equals ().
Oleg Skripnyak du
De plus, dans Equals (), la première ligne doit être "si (! (Obj est Person)) retourne false". Mais la meilleure pratique consiste à utiliser un objet séparé transtypé en un type, comme "var o = obj as Person; if (o == null) return false;" puis vérifiez l'égalité avec o sans lancer
Oleg Skripnyak
1
Remplacer Equals comme celui-ci n'est pas une bonne idée car cela pourrait avoir des conséquences inattendues pour d'autres programmeurs s'attendant à ce que l'égalité de la personne soit déterminée sur plus d'une seule propriété.
B2K
0

Vous devriez pouvoir remplacer Equals on person pour réellement faire Equals on Person.id. Cela devrait entraîner le comportement que vous recherchez.

GWLlosa
la source
-5

Veuillez essayer avec le code ci-dessous.

var Item = GetAll().GroupBy(x => x .Id).ToList();
Mohamed Hammam
la source
3
Une réponse courte est la bienvenue, mais elle n'apportera pas beaucoup de valeur aux derniers utilisateurs qui essaient de comprendre ce qui se passe derrière le problème. Veuillez consacrer du temps à expliquer quel est le véritable problème à l'origine du problème et comment le résoudre. Merci ~
Hearen