Distinct non funziona con 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;
    }

}

Questo si basa su un esempio in "LINQ in Action". Listato 4.16.

Questo stampa Jon Skeet due volte. Perché? Ho anche provato a sovrascrivere il metodo Equals nella classe Author. Still Distinct non sembra funzionare. Cosa mi manca?

Modifica: ho aggiunto anche == e! = Sovraccarico dell'operatore. Ancora nessun aiuto.

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

Risposte:


159

LINQ Distinct non è così intelligente quando si tratta di oggetti personalizzati.

Tutto ciò che fa è guardare il tuo elenco e vedere che ha due oggetti diversi (non importa che abbiano gli stessi valori per i campi dei membri).

Una soluzione alternativa è implementare l'interfaccia IEquatable come mostrato qui .

Se modifichi la tua classe Author in questo modo, dovrebbe funzionare.

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

Provalo come DotNetFiddle


22
IEquatable va bene ma incompleto; dovresti sempre implementare Object.Equals () e Object.GetHashCode () insieme; IEquatable <T> .Equals non sovrascrive Object.Equals, quindi questo fallirà quando si effettuano confronti non fortemente tipizzati, cosa che si verifica spesso nei framework e sempre nelle raccolte non generiche.
AndyM

Quindi è meglio usare l'override di Distinct che prende IEqualityComparer <T> come suggerito da Rex M? Intendo quello che dovrei fare se non voglio cadere nella trappola.
Tanmoy

3
@ Tanmoy dipende. Se vuoi che Author si comporti normalmente come un oggetto normale (cioè solo l'uguaglianza di riferimento) ma controlla i valori del nome per lo scopo di Distinct, usa un IEqualityComparer. Se desideri sempre confrontare gli oggetti Author in base ai valori del nome, esegui l'override di GetHashCode e Equals o implementa IEquatable.
Rex M

3
Ho implementato IEquatable(e sovrascritto Equals/ GetHashCode) ma nessuno dei miei punti di interruzione viene attivato in questi metodi su un Linq Distinct?
PeterX

2
@PeterX l'ho notato anch'io. Ho avuto punti di interruzione in GetHashCodee Equals, sono stati raggiunti durante il ciclo foreach. Questo perché var temp = books.SelectMany(book => book.Authors).Distinct();restituisce un IEnumerable, il che significa che la richiesta non viene eseguita immediatamente, viene eseguita solo quando vengono utilizzati i dati. Se desideri subito un esempio di questo sparo, aggiungi .ToList()dopo .Distinct()e vedrai i punti di interruzione in Equalse GetHashCodeprima del foreach.
JabberwockyDecompiler

70

Il Distinct()metodo controlla l'uguaglianza dei riferimenti per i tipi di riferimento. Ciò significa che sta cercando letteralmente lo stesso oggetto duplicato, non oggetti diversi che contengono gli stessi valori.

Esiste un overload che accetta un IEqualityComparer , quindi puoi specificare una logica diversa per determinare se un determinato oggetto è uguale a un altro.

Se si desidera che Author si comporti normalmente come un oggetto normale (cioè solo l'uguaglianza di riferimento), ma ai fini del controllo distinto dell'uguaglianza in base ai valori del nome, utilizzare un IEqualityComparer . Se desideri sempre confrontare gli oggetti Author in base ai valori del nome, esegui l'override di GetHashCode e Equals o implementa IEquatable .

I due membri IEqualityComparerdell'interfaccia sono Equalse GetHashCode. La tua logica per determinare se due Authoroggetti sono uguali sembra essere se le stringhe Nome e Cognome sono le stesse.

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
Grazie! La tua implementazione di GetHashCode () mi ha mostrato cosa mi mancava ancora. Stavo restituendo {oggetto passato} .GetHashCode (), non {proprietà utilizzata per il confronto} .GetHashCode (). Ciò ha fatto la differenza e spiega perché il mio continuava a fallire: due riferimenti diversi avrebbero due codici hash diversi.
pelazem

44

Un'altra soluzione senza implementare IEquatable, Equalsed GetHashCodeè utilizzare il LINQs GroupBymetodo e per selezionare il primo elemento della 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
mi ha aiutato, considerando solo le prestazioni, funziona alla stessa velocità ?, come considerando i metodi sopra?
Biswajeet

molto più bello che complicarlo con i metodi di implementazione, e se si utilizza EF delegherà il lavoro al server sql.
Zapnologica


@Bellash Fallo funzionare e poi fallo velocemente. Certo, questo raggruppamento può portare a più lavoro da fare. ma a volte è complicato implementare più di quanto si desideri.
Jehof

2
Preferisco questa soluzione ma poi utilizzando un "nuovo" oggetto nel gruppo di: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

C'è un altro modo per ottenere valori distinti dall'elenco di tipi di dati definiti dall'utente:

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

Sicuramente, fornirà una serie distinta di dati


21

Distinct()esegue il confronto di uguaglianza predefinito sugli oggetti nell'enumerabile. Se non hai sovrascritto Equals()e GetHashCode(), utilizza l'implementazione predefinita su object, che confronta i riferimenti.

La soluzione semplice è aggiungere una corretta implementazione di Equals()e GetHashCode()a tutte le classi che partecipano all'oggetto grafico che stai confrontando (es. Libro e Autore).

L' IEqualityComparerinterfaccia è una comodità che ti consente di implementare Equals()e GetHashCode()in una classe separata quando non hai accesso agli interni delle classi che devi confrontare o se stai utilizzando un metodo di confronto diverso.


Grazie mille per questo brillante commento sugli oggetti partecipanti.
suhyura

11

Hai sovrascritto Equals (), ma assicurati di sovrascrivere anche GetHashCode ()


+1 per enfatizzare GetHashCode (). Non aggiungere l'implementazione HashCode di base come in<custom>^base.GetHashCode()
Dani

8

Le risposte precedenti sono sbagliate !!! Distinct come indicato su MSDN restituisce l'equatore predefinito che, come indicato, la proprietà Default controlla se il tipo T implementa l'interfaccia System.IEquatable e, in tal caso, restituisce un EqualityComparer che utilizza tale implementazione. In caso contrario, restituisce un EqualityComparer che utilizza le sostituzioni di Object.Equals e Object.GetHashCode fornite da T

Il che significa che finché esegui l'overide uguale, stai bene.

Il motivo per cui il codice non funziona è perché controlli firstname == lastname.

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


0

È possibile utilizzare il metodo di estensione nell'elenco che verifica l'unicità in base all'hash calcolato. È inoltre possibile modificare il metodo di estensione per supportare IEnumerable.

Esempio:

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. 

Metodo di estensione:

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

-1

Puoi ottenerlo in due modi:

1. Puoi implementare l'interfaccia IEquatable come mostrato nel metodo Enumerable.Distinct oppure puoi vedere la risposta di @ skalb in questo post

2. Se il tuo oggetto non ha una chiave univoca, puoi utilizzare il metodo GroupBy per un elenco di oggetti distinti dell'archivio, che devi raggruppare tutte le proprietà dell'oggetto e dopo selezionare il primo oggetto.

Ad esempio come sotto e funziona per me:

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 è come di seguito:

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

3. Se il tuo oggetto ha una chiave univoca, puoi usarlo solo nel gruppo per.

Ad esempio, la chiave univoca del mio oggetto è Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.