147 votes

Distinct ne fonctionne pas avec LINQ to Objects

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 in Action". Listing 4.16.

Cette empreinte Jon Skeet deux fois. Pourquoi ? J'ai même essayé de remplacer la méthode Equals dans la classe Author. Mais la méthode Distinct ne semble toujours pas fonctionner. Que me manque-t-il ?

Modifier : J'ai également ajouté les surcharges d'opérateurs == et !=. Cela ne m'aide toujours pas.

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

197voto

skalb Points 1195

LINQ Distinct n'est pas si intelligent lorsqu'il s'agit d'objets personnalisés.

Tout ce qu'il fait, c'est regarder votre liste et constater qu'elle contient 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 indiqué ci-dessous aquí .

Si vous modifiez votre classe d'auteur comme suit, 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

27 votes

IEquatable est bien mais incomplet ; vous devriez toujours implémente Object.Equals() et Object.GetHashCode() ensemble ; IEquatable<T>.Equals ne surcharge pas Object.Equals, donc cela échouera lors de comparaisons non fortement typées, ce qui arrive souvent dans les frameworks et toujours dans les collections non-génériques.

0 votes

Est-il donc préférable d'utiliser la surcharge 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.

4 votes

@Tanmoy cela 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érifie les valeurs des noms pour les besoins de Distinct, utilisez un IEqualityComparer. Si vous toujours si vous souhaitez que les objets auteurs soient comparés sur la base des valeurs de nom, il faut alors remplacer GetHashCode et Equals, ou implémenter IEquatable.

77voto

Rex M Points 80372

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

Il existe un 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 que pour les besoins de Distinct, vous vérifiez l'égalité par les valeurs de nom, utilisez une balise IEqualityComparer . Si vous voulez toujours que les objets Auteur soient comparés sur la base des valeurs de nom, alors surcharge GetHashCode et Equals ou implémenter IEquatable .

Les deux membres du IEqualityComparer l'interface sont Equals y GetHashCode . Votre logique pour déterminer si deux Author sont égaux semble être si les chaînes de noms et de prénoms sont les mêmes.

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

1 votes

Merci ! Votre implémentation de GetHashCode() m'a montré ce qui me manquait encore. Je renvoyais {objet passé}.GetHashCode(), et non {propriété utilisée pour la comparaison}.GetHashCode(). Cela a fait la différence et explique pourquoi la mienne échouait toujours - deux références différentes auraient deux codes de hachage différents.

58voto

Jehof Points 14720

Une autre solution sans mise en œuvre IEquatable , Equals y GetHashCode est d'utiliser les LINQs GroupBy et de 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);
}

1 votes

Cela m'a aidé, mais en considérant les performances, est-ce que cela fonctionne à la même vitesse que les méthodes ci-dessus ?

0 votes

C'est beaucoup plus agréable que de le compliquer avec des méthodes d'implémentation, et si vous utilisez EF, vous déléguerez le travail au serveur sql.

0 votes

Bien que cette méthode puisse fonctionner, il y aura un problème de performance en raison du nombre d'éléments regroupés.

26voto

AndyM Points 741

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

La solution simple consiste à ajouter un correct mise en œuvre de Equals() y GetHashCode() à toutes les classes qui participent au graphe d'objets que vous comparez (c'est-à-dire Livre et Auteur).

El IEqualityComparer est une interface pratique qui vous permet d'implémenter Equals() y GetHashCode() dans une classe séparée 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.

0 votes

Merci beaucoup pour ce commentaire brillant sur les objets participatifs.

13voto

Eric King Points 4937

Vous avez surchargé Equals(), mais assurez-vous de surcharger également GetHashCode().

1 votes

+1 pour avoir mis l'accent sur GetHashCode(). Ne pas ajouter l'implémentation de base de HashCode comme dans <custom>^base.GetHashCode()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X