Je joue avec LINQ pour en apprendre davantage à ce sujet, mais je ne peux pas comprendre comment utiliser Distinct quand je n’ai pas une liste simple (une simple liste d’entiers est assez facile à faire, ce n’est pas la question). Qu'est-ce que je veux si je veux utiliser Distinct sur la liste d'un objet sur un ou plus propriétés de l'objet?
Exemple: Si un objet est Person
, avec la propriété Id
. Comment obtenir toute la personne et utiliser Distinct
avec la propriété Id
de l'objet?
Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"
Comment puis-je obtenir juste Person1 et Person3? Est-ce possible?
Si ce n’est pas possible avec LINQ, quel serait le meilleur moyen d’obtenir une liste de Person
en fonction de certaines de ses propriétés dans .NET 3.5?
EDIT: Cela fait maintenant partie de MoreLINQ .
Ce dont vous avez besoin est un "distinct" par efficacement. Je ne crois pas que cela fasse partie de LINQ en l'état, 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 seulement la propriété Id
, 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 l'égalité de manière appropriée:
var query = people.DistinctBy(p => new { p.Id, p.Name });
Non testé, mais cela devrait fonctionner (et maintenant, il compile au moins).
Il assume cependant le comparateur par défaut pour les clés - si vous souhaitez passer un comparateur d’égalité, transmettez-le simplement au constructeur HashSet
.
Et si je veux obtenir une liste distincte basée sur les propriétés n ou plus?
Facile! Vous voulez les regrouper et choisir un gagnant parmi 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();
Vous pouvez également utiliser la syntaxe de la requête si vous souhaitez qu'elle ressemble à celle de 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();
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());
La where
vous aide à filtrer les entrées (peut-être plus complexe) et les fonctions groupby
et select
remplissent une fonction distincte.
Je pense que c'est assez:
list.Select(s => s.MyField).Distinct();
Solutionz d'abord le groupe par 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();
Vous pouvez le faire avec le standard Linq.ToLookup()
. Cela créera une collection de valeurs pour chaque clé unique. Il suffit de sélectionner le premier élément de la collection
Persons.ToLookup(p => p.Id).Select(coll => coll.First());
Le code suivant est fonctionnellement équivalent à réponse de Jon Skeet .
Testé sur .NET 4.5, devrait fonctionner sur toute 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)));
}
Incidemment, consultez la dernière version de DistinctBy.cs par Jon Skeet sur Google Code .
J'ai écrit un article qui explique comment étendre la fonction Distinct afin de pouvoir procéder 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
Vous pouvez le faire (bien que pas rapidement) comme ceci:
people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));
Autrement dit, "sélectionnez toutes les personnes pour lesquelles il n'y a pas d'autre personne différente dans la liste portant le même identifiant".
Notez que, dans votre exemple, 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.
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'utilisation prévue:
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 classe LambdaEqualityComparer
pour d’autres méthodes acceptant un IEqualityComparer
. (Oh, et je laisse la chose yield
à l'implémentation originale de LINQ ...)
Si vous avez besoin d'une méthode Distinct sur plusieurs propriétés, vous pouvez consulter ma bibliothèque PowerfulExtensions . Actuellement, il est encore très jeune, mais vous pouvez déjà utiliser des méthodes telles que Distinct, Union, Intersect, Sauf sur un nombre quelconque de propriétés;
Voici comment vous l'utilisez:
using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Lorsque nous avons dû faire face à une telle tâche dans notre projet, nous avons défini une petite API pour composer des comparateurs.
Ainsi, le cas d'utilisation était le suivant:
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 in LINQ.
Si vous ne souhaitez pas ajouter la bibliothèque MoreLinq à votre projet uniquement pour obtenir la fonctionnalité DistinctBy
, vous pouvez obtenir le même résultat final en utilisant la surcharge de la méthode Distinct
de Linq qui prend en compte un IEqualityComparer
argument.
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 ceci:
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:
Person.Id
est de type int
people
ne contient aucun élément nullSi la collection peut contenir des valeurs null, il suffit de réécrire les lambdas pour rechercher la valeur null, par exemple:
Func<Person, Person, bool> areEqual = (p1, p2) =>
{
return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};
EDIT
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.
Toutefois, si vos objets ne peuvent différer que de Id
, un autre utilisateur vous a répondu que tout ce que vous avez à faire est de remplacer les implémentations par défaut de GetHashCode()
et Equals()
dans votre Person
classe, puis utilisez simplement la méthode Distinct()
prête à l'emploi de Linq pour filtrer les doublons.
List<Person>lst=new List<Person>
var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
Remplacer Equals (object obj) et GetHashCode () méthodes:
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();
}
}
et ensuite il suffit d'appeler:
List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();
Le meilleur moyen de le faire, compatible avec les autres versions .NET, consiste à remplacer Equals et GetHash pour le gérer (voir Question relative au débordement de la pile 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 tout votre code, les solutions présentées dans cet article sont géniales.
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:
using Microsoft.Ajax.Utilities;
et ensuite 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.
Vous devriez être en mesure de remplacer Equals on person pour faire Equals on Person.id. Cela devrait entraîner le comportement que vous recherchez.
List<string> colors = new List<string> { "blue", "red", "black", "blue", "yellow", "blue" };
IEnumerable<string> distinctColors = colors.Distinct();