Distinct ne fonctionne pas avec LINQ to Objects

120
class Program
{
    static void Main(string[] args)
    {
        List<Book> books = new List<Book> 
        {
            new Book
            {
                Name="C# in Depth",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },
                     new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },                       
                }
            },
            new Book
            {
                Name="LINQ in Action",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Fabrice", LastName="Marguerie"
                    },
                     new Author 
                    {
                        FirstName = "Steve", LastName="Eichert"
                    },
                     new Author 
                    {
                        FirstName = "Jim", LastName="Wooley"
                    },
                }
            },
        };


        var temp = books.SelectMany(book => book.Authors).Distinct();
        foreach (var author in temp)
        {
            Console.WriteLine(author.FirstName + " " + author.LastName);
        }

        Console.Read();
    }

}
public class Book
{
    public string Name { get; set; }
    public List<Author> Authors { get; set; }
}
public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object obj)
    {
        return true;
        //if (obj.GetType() != typeof(Author)) return false;
        //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName;
    }

}

Ceci est basé sur un exemple dans "LINQ en action". Listing 4.16.

Cela imprime Jon Skeet deux fois. Pourquoi? J'ai même essayé de remplacer la méthode Equals dans la classe Author. Still Distinct ne semble pas fonctionner. Qu'est-ce que je rate?

Edit: J'ai également ajouté == et! = Surcharge d'opérateurs. Toujours pas d'aide.

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }
Tanmoy
la source

Réponses:

159

LINQ Distinct n'est pas si intelligent en ce qui concerne les objets personnalisés.

Tout ce qu'il fait est de regarder votre liste et de voir qu'elle a deux objets différents (peu importe qu'ils aient les mêmes valeurs pour les champs membres).

Une solution de contournement consiste à implémenter l'interface IEquatable comme illustré ici .

Si vous modifiez votre classe Author comme cela, cela devrait fonctionner.

public class Author : IEquatable<Author>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public bool Equals(Author other)
    {
        if (FirstName == other.FirstName && LastName == other.LastName)
            return true;

        return false;
    }

    public override int GetHashCode()
    {
        int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode();
        int hashLastName = LastName == null ? 0 : LastName.GetHashCode();

        return hashFirstName ^ hashLastName;
    }
}

Essayez-le comme DotNetFiddle

skalb
la source
22
IEquatable est correct mais incomplet; vous devez toujours implémenter Object.Equals () et Object.GetHashCode () ensemble; IEquatable <T> .Equals ne remplace pas Object.Equals, donc cela échouera lors de comparaisons non fortement typées, ce qui se produit souvent dans des frameworks et toujours dans des collections non génériques.
AndyM
Est-il donc préférable d'utiliser le remplacement de Distinct qui prend IEqualityComparer <T> comme Rex M l'a suggéré? Je veux dire ce que je devrais faire si je ne veux pas tomber dans le piège.
Tanmoy le
3
@Tanmoy ça dépend. Si vous voulez que Author se comporte normalement comme un objet normal (c'est-à-dire uniquement l'égalité de référence) mais vérifiez les valeurs de nom dans le but de Distinct, utilisez un IEqualityComparer. Si vous souhaitez toujours que les objets Author soient comparés en fonction des valeurs de nom, remplacez GetHashCode et Equals, ou implémentez IEquatable.
Rex M le
3
J'ai implémenté IEquatable(et remplacé Equals/ GetHashCode) mais aucun de mes points d'arrêt ne se déclenche dans ces méthodes sur un Linq Distinct?
PeterX
2
@PeterX J'ai remarqué cela aussi. J'avais des points d'arrêt dans le GetHashCodeet Equals, ils ont été touchés pendant la boucle foreach. En effet, le var temp = books.SelectMany(book => book.Authors).Distinct();retourne un IEnumerable, ce qui signifie que la demande n'est pas exécutée tout de suite, elle n'est exécutée que lorsque les données sont utilisées. Si vous souhaitez un exemple de ce déclenchement tout de suite, ajoutez .ToList()après le .Distinct()et vous verrez les points d'arrêt dans Equalset GetHashCodeavant le foreach.
JabberwockyDecompiler
70

La Distinct()méthode vérifie l'égalité de référence pour les types de référence. Cela signifie qu'il recherche littéralement le même objet dupliqué, et non des objets différents contenant les mêmes valeurs.

Il existe une surcharge qui prend un IEqualityComparer , vous pouvez donc spécifier une logique différente pour déterminer si un objet donné est égal à un autre.

Si vous voulez que Author se comporte normalement comme un objet normal (c'est-à-dire uniquement l'égalité de référence), mais aux fins de l'égalité de contrôle distincte par valeurs de nom, utilisez un IEqualityComparer . Si vous souhaitez toujours que les objets Author soient comparés en fonction des valeurs de nom, remplacez GetHashCode et Equals , ou implémentez IEquatable .

Les deux membres de l' IEqualityComparerinterface sont Equalset GetHashCode. Votre logique pour déterminer si deux Authorobjets sont égaux semble être si les chaînes de prénom et de nom sont identiques.

public class AuthorEquals : IEqualityComparer<Author>
{
    public bool Equals(Author left, Author right)
    {
        if((object)left == null && (object)right == null)
        {
            return true;
        }
        if((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.FirstName == right.FirstName && left.LastName == right.LastName;
    }

    public int GetHashCode(Author author)
    {
        return (author.FirstName + author.LastName).GetHashCode();
    }
}
Rex M
la source
1
Je vous remercie! Votre implémentation GetHashCode () m'a montré ce qui me manquait toujours. Je retournais {objet passé} .GetHashCode (), pas {propriété utilisée pour la comparaison} .GetHashCode (). Cela a fait la différence et explique pourquoi le mien échouait toujours - deux références différentes auraient deux codes de hachage différents.
pelazem
44

Une autre solution sans implémentation IEquatable, Equalset GetHashCodeconsiste à utiliser la GroupByméthode LINQs et à sélectionner le premier élément de l'IGrouping.

var temp = books.SelectMany(book => book.Authors)
                .GroupBy (y => y.FirstName + y.LastName )
                .Select (y => y.First ());

foreach (var author in temp){
  Console.WriteLine(author.FirstName + " " + author.LastName);
}
Jehof
la source
1
cela m'a aidé, juste en considérant les performances, est-ce que cela fonctionne à la même vitesse ?, comme en considérant les méthodes ci-dessus?
Biswajeet le
beaucoup plus agréable que de le compliquer avec des méthodes d'implémentation, et si vous utilisez EF, le travail sera délégué au serveur sql.
Zapnologica
bien que cette méthode puisse fonctionner, il y aura un problème de performance en raison du nombre de choses groupées
Bellash
@Bellash Faites-le fonctionner puis faites-le vite. Bien sûr, ce regroupement peut conduire à plus de travail à faire. mais parfois, il est difficile de mettre en œuvre plus que vous ne le souhaitez.
Jehof
2
Je préfère cette solution mais ensuite en utilisant un "nouvel" objet dans le groupby: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong le
32

Il existe un autre moyen d'obtenir des valeurs distinctes à partir de la liste des types de données définis par l'utilisateur:

YourList.GroupBy(i => i.Id).Select(i => i.FirstOrDefault()).ToList();

Sûrement, cela donnera un ensemble distinct de données

Ashu_90
la source
21

Distinct()effectue la comparaison d'égalité par défaut sur les objets de l'énumérable. Si vous n'avez pas remplacé Equals()et GetHashCode(), il utilise l'implémentation par défaut on object, qui compare les références.

La solution simple est d'ajouter une implémentation correcte de Equals()et GetHashCode()à toutes les classes qui participent au graphe d'objets que vous comparez (c'est-à-dire Book et Author).

L' IEqualityComparerinterface est une commodité qui vous permet d'implémenter Equals()et GetHashCode()dans une classe distincte lorsque vous n'avez pas accès aux internes des classes que vous devez comparer, ou si vous utilisez une méthode de comparaison différente.

AndyM
la source
Merci beaucoup pour ce brillant commentaire sur les objets participants.
suhyura le
11

Vous avez remplacé Equals (), mais assurez-vous de remplacer également GetHashCode ()

Eric King
la source
+1 pour mettre en valeur GetHashCode (). N'ajoutez pas l'implémentation de HashCode de base comme dans<custom>^base.GetHashCode()
Dani
8

Les réponses ci-dessus sont fausses !!! Distinct comme indiqué sur MSDN renvoie l'équateur par défaut qui, comme indiqué. La propriété Default vérifie si le type T implémente l'interface System.IEquatable et, si tel est le cas, renvoie un EqualityComparer qui utilise cette implémentation. Sinon, il renvoie un EqualityComparer qui utilise les substitutions de Object.Equals et Object.GetHashCode fournis par T

Ce qui signifie que tant que vous remplacez Égal, tout va bien.

La raison pour laquelle votre code ne fonctionne pas est que vous vérifiez firstname == lastname.

voir https://msdn.microsoft.com/library/bb348436(v=vs.100).aspx et https://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx

Alex
la source
0

Vous pouvez utiliser la méthode d'extension sur la liste qui vérifie l'unicité en fonction du hachage calculé. Vous pouvez également modifier la méthode d'extension pour prendre en charge IEnumerable.

Exemple:

public class Employee{
public string Name{get;set;}
public int Age{get;set;}
}

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

Méthode d'extension:

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }
chindirala sampath kumar
la source
-1

Vous pouvez y parvenir de deux manières:

1. Vous pouvez implémenter l'interface IEquatable comme indiqué Méthode Enumerable.Distinct ou vous pouvez voir la réponse de @ skalb à ce poste

2. Si votre objet n'a pas de clé unique, vous pouvez utiliser la méthode GroupBy pour obtenir une liste d'objets distincts, que vous devez regrouper toutes les propriétés de l'objet et après avoir sélectionné le premier objet.

Par exemple, comme ci-dessous et travaillant pour moi:

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

La classe MyObject est comme ci-dessous:

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

3. Si votre objet a une clé unique, vous ne pouvez l'utiliser qu'en groupe.

Par exemple, la clé unique de mon objet est Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Ramil Aliyev
la source