2009-09-02 16 views
88
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; 
    } 

} 

Esto se basa en un ejemplo en "LINQ in Action". Listado 4.16.Distinto no funciona con LINQ to Objects

Esto imprime Jon Skeet dos veces. ¿Por qué? Incluso he intentado anular el método Equals en la clase de Autor. Still Distinct no parece funcionar. ¿Qué me estoy perdiendo?

Edit: He añadido == y! = Sobrecarga del operador también. Todavía no hay ayuda.

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

Respuesta

114

LINQ Distinct no es tan inteligente cuando se trata de objetos personalizados.

Todo lo que hace es mirar su lista y ver que tiene dos objetos diferentes (no importa que tengan los mismos valores para los campos de miembros).

Una solución consiste en implementar la interfaz IEquatable como se muestra en here.

Si modifica su clase Author así debería funcionar.

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

Try it as DotNetFiddle

+11

IEtabletable está bien pero incompleto; debe * siempre * implementar Object.Equals() y Object.GetHashCode() juntos; IEtabletable .Equals no anula Object.Equals, por lo que esto no funcionará al realizar comparaciones no fuertemente tipadas, lo que ocurre a menudo en frameworks y siempre en colecciones no genéricas. – AndyM

+0

Entonces, ¿es mejor usar la anulación de Distinct que toma IEqualityComparer como lo ha sugerido Rex M? Quiero decir lo que debería estar haciendo si no quiero caer en la trampa. – Tanmoy

+3

@Tanmoy depende. Si desea que el Autor se comporte normalmente como un objeto normal (es decir, solo igualdad de referencia), pero verifique los valores del nombre con el propósito de Distinct, use un IEqualityComparer. Si * siempre * quiere que se comparen objetos de Autor en función de los valores de nombre, entonces anule GetHashCode e Igual, o implemente IEquatable. –

54

El método Distinct() comprueba la igualdad de referencia para los tipos de referencia. Esto significa que está buscando literalmente el mismo objeto duplicado, no diferentes objetos que contienen los mismos valores.

Hay un overload que toma un IEqualityComparer, por lo que puede especificar una lógica diferente para determinar si un objeto dado es igual a otro.

Si desea que el Autor se comporte normalmente como un objeto normal (es decir, solo igualdad de referencia), pero para propósitos de Igualdad de verificación distintiva por valores de nombre, use un IEqualityComparer. Si siempre quiere que se comparen los objetos Autor en función de los valores del nombre, entonces anula GetHashCode e Igual, o implementa IEquatable.

Los dos miembros en la interfaz IEqualityComparer son Equals y GetHashCode. Su lógica para determinar si dos objetos Author son iguales parece ser si las cadenas de Nombre y Apellido son las mismas.

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

Gracias ! Su implementación de GetHashCode() me mostró lo que aún me faltaba. Estaba devolviendo {pass-in object} .GetHashCode(), no {propiedad que se utiliza para la comparación} .GetHashCode(). Eso marcó la diferencia y explica por qué el mío todavía estaba fallando: dos referencias diferentes tendrían dos códigos hash diferentes. – pelazem

9

Usted ha anulado equals(), pero asegúrese de que también reemplazar GetHashCode()

+0

+1 para enfatizar GetHashCode(). No agregue la implementación base de HashCode como en '^base.GetHashCode() ' – Dani

20

Distinct() realiza la comparación de igualdad predeterminado sobre los objetos del enumerable. Si no ha reemplazado Equals() y GetHashCode(), utiliza la implementación predeterminada en object, que compara las referencias.

La solución más sencilla es añadir una aplicación correcta de Equals() y GetHashCode() a todas las clases que participan en el gráfico de objetos que está comparando (es decir, del libro y del autor).

La interfaz IEqualityComparer es una comodidad que le permite implementar Equals() y GetHashCode() en una clase separada cuando usted no tiene acceso a la parte interna de las clases que necesita para comparar, o si está utilizando un método diferente de la comparación .

+0

Muchas gracias por este excelente comentario sobre los objetos participantes. – suhyura

33

Otra solución sin implementar IEquatable, Equals y GetHashCode es utilizar el método LINQs GroupBy y para seleccionar el primer elemento de la 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

me ayudó, solo considerando el rendimiento, ¿funciona a la misma velocidad ?, ¿considerando los métodos anteriores? – Biswajeet

+0

es mucho mejor que complicarlo con los métodos de implementación, y si usa EF delegará el trabajo al servidor sql. – Zapnologica

+0

mientras este método puede funcionar, habrá un problema de rendimiento debido a la cantidad de elementos que se agrupan – Bellash

5

¡Las respuestas de Above están equivocadas! Distinto como se indica en MSDN devuelve el ecuador predeterminado que, como se indica La propiedad predeterminada comprueba si el tipo T implementa la interfaz System.IEquatable y, en caso afirmativo, devuelve un EqualityComparer que utiliza esa implementación. De lo contrario, devuelve un EqualityComparer que utiliza las anulaciones de las Object.equals y Object.GetHashCode proporcionados por T

Lo que significa que todo el tiempo que overide Es igual a que estés bien.

La razón por la que su código no funciona es porque verifica el nombre == apellido.

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

7

Hay otra manera de obtener valores distintos de la lista de usuario tipo de datos definido:

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

Sin duda, dará conjunto distinto de datos