Deutlich nicht mit LINQ to Objects arbeiten

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

}

Dies basiert auf einem Beispiel in "LINQ in Action". Listing 4.16.

Dies druckt Jon Skeet zweimal. Warum? Ich habe sogar versucht, die Equals-Methode in der Author-Klasse zu überschreiben. Trotzdem scheint Distinct nicht zu funktionieren. Was vermisse ich?

Edit: Ich habe auch == und! = Operatorüberladung hinzugefügt. Immer noch keine Hilfe.

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

Antworten:

159

LINQ Distinct ist nicht so schlau, wenn es um benutzerdefinierte Objekte geht.

Sie müssen lediglich Ihre Liste überprüfen und feststellen, dass sie zwei verschiedene Objekte enthält (es ist egal, dass sie dieselben Werte für die Mitgliedsfelder haben).

Eine Problemumgehung besteht darin, die IEquatable-Schnittstelle wie hier gezeigt zu implementieren .

Wenn Sie Ihre Autorenklasse so ändern, sollte es funktionieren.

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

Versuchen Sie es als DotNetFiddle

skalb
quelle
22
IEquatable ist in Ordnung, aber unvollständig; Sie sollten immer implemement Object.Equals () und Object.GetHashCode () zusammen; IEquatable <T> .Equals überschreibt Object.Equals nicht. Dies schlägt daher fehl, wenn nicht stark typisierte Vergleiche durchgeführt werden. Dies tritt häufig in Frameworks und immer in nicht generischen Sammlungen auf.
AndyM
Ist es also besser, die Überschreibung von Distinct zu verwenden, die IEqualityComparer <T> verwendet, wie Rex M vorgeschlagen hat? Ich meine, was ich tun soll, wenn ich nicht in die Falle tappen will.
Tanmoy
3
@ Tanmoy es kommt darauf an. Wenn Sie möchten, dass sich der Autor normal wie ein normales Objekt verhält (dh nur die Referenzgleichheit), aber die Namenswerte für Distinct überprüfen, verwenden Sie einen IEqualityComparer. Wenn Sie immer möchten, dass Author-Objekte basierend auf den Namenswerten verglichen werden, überschreiben Sie GetHashCode und Equals oder implementieren Sie IEquatable.
Rex M
3
Ich habe implementiert IEquatable(und überschrieben Equals/ GetHashCode), aber keiner meiner Haltepunkte wird in diesen Methoden auf einem Linq ausgelöst Distinct.
PeterX
2
@ PeterX Das ist mir auch aufgefallen. Ich hatte Haltepunkte in der GetHashCodeund Equalssie wurden während der foreach-Schleife getroffen. Dies liegt daran, dass die var temp = books.SelectMany(book => book.Authors).Distinct();Rückgabe an erfolgt IEnumerable, was bedeutet, dass die Anforderung nicht sofort ausgeführt wird, sondern nur, wenn die Daten verwendet werden. Wenn Sie sofort ein Beispiel für dieses Brennen wünschen, fügen Sie es .ToList()nach dem hinzu, .Distinct()und Sie sehen die Haltepunkte im Equalsund GetHashCodevor dem foreach.
JabberwockyDecompiler
70

Die Distinct()Methode überprüft die Referenzgleichheit auf Referenztypen. Dies bedeutet, dass buchstäblich dasselbe Objekt dupliziert wird, nicht verschiedene Objekte, die dieselben Werte enthalten.

Es gibt eine Überladung, die einen IEqualityComparer erfordert , sodass Sie eine andere Logik angeben können, um zu bestimmen, ob ein bestimmtes Objekt einem anderen entspricht.

Wenn Sie möchten, dass sich der Autor normalerweise wie ein normales Objekt verhält (dh nur die Referenzgleichheit), aber zum Zwecke der eindeutigen Überprüfung der Gleichheit anhand von Namenswerten einen IEqualityComparer verwenden . Wenn Sie immer möchten, dass Author-Objekte basierend auf den Namenswerten verglichen werden, überschreiben Sie GetHashCode und Equals oder implementieren Sie IEquatable .

Die beiden Mitglieder der IEqualityComparerBenutzeroberfläche sind Equalsund GetHashCode. Ihre Logik zum Bestimmen, ob zwei AuthorObjekte gleich sind, scheint zu sein, wenn die Vor- und Nachnamenzeichenfolgen gleich sind.

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.
quelle
1
Danke dir! Ihre GetHashCode () -Implementierung hat mir gezeigt, was mir noch fehlte. Ich habe {übergebenes Objekt} .GetHashCode () zurückgegeben, nicht {Eigenschaft, die zum Vergleich verwendet wird} .GetHashCode (). Das machte den Unterschied und erklärt, warum meine immer noch fehlschlug - zwei verschiedene Referenzen hätten zwei verschiedene Hash-Codes.
Pelazem
44

Eine weitere Lösung ohne Umsetzung IEquatable, Equalsund GetHashCodeist die LINQs zu verwenden GroupByMethode und das erste Element aus der IGrouping auszuwählen.

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
quelle
1
Es hat mir geholfen, nur unter Berücksichtigung der Leistung, funktioniert dies mit der gleichen Geschwindigkeit wie unter Berücksichtigung der oben genannten Methoden?
Biswajeet
viel schöner als es mit Implementierungsmethoden zu komplizieren, und wenn EF verwendet wird, wird die Arbeit an den SQL Server delegiert.
Zapnologica
Während diese Methode möglicherweise funktioniert, wird es aufgrund der Anzahl der zu gruppierenden Dinge ein Leistungsproblem geben
Bellash
@ Bellash Lass es funktionieren und mach es dann schnell. Sicherlich kann diese Gruppierung dazu führen, dass mehr Arbeit erledigt werden muss. Manchmal ist es jedoch umständlich, mehr zu implementieren, als Sie möchten.
Jehof
2
Ich bevorzuge diese Lösung, aber dann, indem ich ein "neues" Objekt in der Gruppe verwende: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong
32

Es gibt noch eine Möglichkeit, eindeutige Werte aus der Liste der benutzerdefinierten Datentypen abzurufen:

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

Sicherlich wird es unterschiedliche Daten geben

Ashu_90
quelle
21

Distinct()führt den Standardgleichheitsvergleich für Objekte in der Aufzählung durch. Wenn Sie nicht außer Kraft gesetzt haben Equals()und GetHashCode()dann verwendet er die Standardimplementierung auf object, die Referenzen vergleicht.

Die einfache Lösung besteht darin , allen Klassen, die an dem zu vergleichenden Objektdiagramm beteiligt sind (z. B. Buch und Autor) , eine korrekte Implementierung von Equals()und hinzuzufügen GetHashCode().

Die IEqualityComparerSchnittstelle ist eine Bequemlichkeit , die Sie implementieren können Equals()und GetHashCode()in einer separaten Klasse , wenn Sie haben keinen Zugriff auf die Interna der Klassen , die Sie vergleichen müssen, oder wenn Sie eine andere Methode des Vergleichs verwendet wird .

AndyM
quelle
Vielen Dank für diesen brillanten Kommentar zu den teilnehmenden Objekten.
Suhyura
11

Sie haben Equals () überschrieben, aber stellen Sie sicher, dass Sie auch GetHashCode () überschreiben.

Eric King
quelle
+1 für die Hervorhebung von GetHashCode (). Fügen Sie die Basis-HashCode-Implementierung nicht wie in<custom>^base.GetHashCode()
Dani
8

Die obigen Antworten sind falsch !!! Distinct wie in MSDN angegeben gibt den Standardäquator zurück, der wie angegeben Die Default-Eigenschaft prüft, ob Typ T die System.IEquatable-Schnittstelle implementiert, und gibt in diesem Fall einen EqualityComparer zurück, der diese Implementierung verwendet. Andernfalls wird ein EqualityComparer zurückgegeben, der die von T bereitgestellten Überschreibungen von Object.Equals und Object.GetHashCode verwendet

Was bedeutet, solange Sie Equals überschreiben, geht es Ihnen gut.

Der Grund, warum Ihr Code nicht funktioniert, ist, dass Sie Vorname == Nachname überprüfen.

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

Alex
quelle
0

Sie können die Erweiterungsmethode für Listen verwenden, die die Eindeutigkeit basierend auf dem berechneten Hash überprüft. Sie können die Erweiterungsmethode auch ändern, um IEnumerable zu unterstützen.

Beispiel:

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. 

Verlängerungsmethode:

    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
quelle
-1

Sie können dies auf zwei Arten erreichen:

1. Sie können die IEquatable-Schnittstelle wie in der Enumerable.Distinct-Methode gezeigt implementieren, oder Sie können die Antwort von @ skalb in diesem Beitrag sehen

2. Wenn Ihr Objekt keinen eindeutigen Schlüssel hat, können Sie die GroupBy-Methode verwenden, um eine eindeutige Objektliste zu erhalten. Sie müssen alle Eigenschaften des Objekts gruppieren und nach Auswahl des ersten Objekts.

Zum Beispiel wie unten und für mich arbeiten:

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

Die MyObject-Klasse sieht wie folgt aus:

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

3. Wenn Ihr Objekt einen eindeutigen Schlüssel hat, können Sie ihn nur in Gruppe nach verwenden.

Zum Beispiel ist der eindeutige Schlüssel meines Objekts Id.

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