LINQ: valeurs distinctes

136

J'ai l'élément suivant défini à partir d'un XML:

id           category

5            1
5            3
5            4
5            3
5            3

J'ai besoin d'une liste distincte de ces éléments:

5            1
5            3
5            4

Comment puis-je distinguer la catégorie ET l'identifiant également dans LINQ?

balint
la source

Réponses:

221

Essayez-vous d'être distinct par plus d'un champ? Si tel est le cas, utilisez simplement un type anonyme et l'opérateur Distinct et cela devrait être correct:

var query = doc.Elements("whatever")
               .Select(element => new {
                             id = (int) element.Attribute("id"),
                             category = (int) element.Attribute("cat") })
               .Distinct();

Si vous essayez d'obtenir un ensemble distinct de valeurs d'un type "plus grand", mais que vous ne regardez qu'un sous-ensemble de propriétés pour l'aspect de la distinction, vous souhaiterez probablement l' DistinctByimplémentation dans MoreLINQ dans DistinctBy.cs:

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

(Si vous passez en nulltant que comparateur, il utilisera le comparateur par défaut pour le type de clé.)

Jon Skeet
la source
Oh, donc par "plus grand type", vous pouvez vouloir dire que je veux toujours toutes les propriétés dans le résultat même si je veux seulement comparer quelques propriétés pour déterminer la distinction?
The Red Pea
@TheRedPea: Oui, exactement.
Jon Skeet
27

En plus de la réponse de Jon Skeet, vous pouvez également utiliser les expressions group by pour obtenir les groupes uniques avec un décompte pour chaque itération de groupes:

var query = from e in doc.Elements("whatever")
            group e by new { id = e.Key, val = e.Value } into g
            select new { id = g.Key.id, val = g.Key.val, count = g.Count() };
James Alexander
la source
4
Vous avez écrit "en plus de la réponse de Jon Skeet" ... Je ne sais pas si une telle chose est possible. ;)
Yehuda Makarov
13

Pour tous ceux qui cherchent encore; voici une autre façon d'implémenter un comparateur lambda personnalisé.

public class LambdaComparer<T> : IEqualityComparer<T>
    {
        private readonly Func<T, T, bool> _expression;

        public LambdaComparer(Func<T, T, bool> lambda)
        {
            _expression = lambda;
        }

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

        public int GetHashCode(T obj)
        {
            /*
             If you just return 0 for the hash the Equals comparer will kick in. 
             The underlying evaluation checks the hash and then short circuits the evaluation if it is false.
             Otherwise, it checks the Equals. If you force the hash to be true (by assuming 0 for both objects), 
             you will always fall through to the Equals check which is what we are always going for.
            */
            return 0;
        }
    }

vous pouvez ensuite créer une extension pour le linq Distinct qui peut prendre en lambda

   public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list,  Func<T, T, bool> lambda)
        {
            return list.Distinct(new LambdaComparer<T>(lambda));
        }  

Usage:

var availableItems = list.Distinct((p, p1) => p.Id== p1.Id);
Ricky G
la source
En regardant la source de référence, Distinct utilise un ensemble de hachage pour stocker les éléments qu'il a déjà générés. Toujours renvoyer le même code de hachage signifie que chaque élément précédemment retourné est examiné à chaque fois. Un code de hachage plus robuste accélérerait les choses car il se comparerait uniquement aux éléments du même compartiment de hachage. Zéro est une valeur par défaut raisonnable, mais cela peut valoir la peine de prendre en charge un deuxième lambda pour le code de hachage.
Darryl
Bon point! J'essaierai de modifier quand j'aurai le temps, si vous travaillez dans ce domaine pour le moment, n'hésitez pas à modifier
Ricky G
8

Je suis un peu en retard pour la réponse, mais vous voudrez peut-être le faire si vous voulez l'élément entier, pas seulement les valeurs que vous souhaitez regrouper:

var query = doc.Elements("whatever")
               .GroupBy(element => new {
                             id = (int) element.Attribute("id"),
                             category = (int) element.Attribute("cat") })
               .Select(e => e.First());

Cela vous donnera le premier élément entier correspondant à votre groupe par sélection, un peu comme le deuxième exemple de Jon Skeets utilisant DistinctBy, mais sans implémenter le comparateur IEqualityComparer. DistinctBy sera probablement plus rapide, mais la solution ci-dessus impliquera moins de code si les performances ne sont pas un problème.

Olle Johansson
la source
4
// First Get DataTable as dt
// DataRowComparer Compare columns numbers in each row & data in each row

IEnumerable<DataRow> Distinct = dt.AsEnumerable().Distinct(DataRowComparer.Default);

foreach (DataRow row in Distinct)
{
    Console.WriteLine("{0,-15} {1,-15}",
        row.Field<int>(0),
        row.Field<string>(1)); 
}
Mohamed Elsayed
la source
0

Puisque nous parlons d'avoir chaque élément exactement une fois, un «ensemble» a plus de sens pour moi.

Exemple avec des classes et IEqualityComparer implémentés:

 public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public Product(int x, string y)
        {
            Id = x;
            Name = y;
        }
    }

    public class ProductCompare : IEqualityComparer<Product>
    {
        public bool Equals(Product x, Product y)
        {  //Check whether the compared objects reference the same data.
            if (Object.ReferenceEquals(x, y)) return true;

            //Check whether any of the compared objects is null.
            if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
                return false;

            //Check whether the products' properties are equal.
            return x.Id == y.Id && x.Name == y.Name;
        }
        public int GetHashCode(Product product)
        {
            //Check whether the object is null
            if (Object.ReferenceEquals(product, null)) return 0;

            //Get hash code for the Name field if it is not null.
            int hashProductName = product.Name == null ? 0 : product.Name.GetHashCode();

            //Get hash code for the Code field.
            int hashProductCode = product.Id.GetHashCode();

            //Calculate the hash code for the product.
            return hashProductName ^ hashProductCode;
        }
    }

Maintenant

List<Product> originalList = new List<Product> {new Product(1, "ad"), new Product(1, "ad")};
var setList = new HashSet<Product>(originalList, new ProductCompare()).ToList();

setList aura des éléments uniques

J'ai pensé à cela en traitant de .Except()ce qui renvoie une différence d'ensemble

Aditya AVS
la source