Distinto no funciona 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;
    }

}

Esto se basa en un ejemplo de "LINQ en acción". Listado 4.16.

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

Editar: también he agregado == y! = Sobrecarga del operador. Sigo sin ayuda.

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

Respuestas:

159

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 le importa que tengan los mismos valores para los campos miembros).

Una solución es implementar la interfaz IEquatable como se muestra aquí .

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

Pruébelo como DotNetFiddle

skalb
fuente
22
IEquatable está bien pero incompleto; siempre debe implementar Object.Equals () y Object.GetHashCode () juntos; IEquatable <T> .Equals no anula Object.Equals, por lo que esto fallará al hacer comparaciones no fuertemente tipadas, lo que ocurre a menudo en marcos y siempre en colecciones no genéricas.
AndyM
Entonces, ¿es mejor usar la anulación de Distinct que toma IEqualityComparer <T> como Rex M ha sugerido? Me refiero a lo que debería estar haciendo si no quiero caer en la trampa.
Tanmoy
3
@Tanmoy depende. Si desea que Author se comporte normalmente como un objeto normal (es decir, solo la igualdad de referencia) pero verifique los valores del nombre para el propósito de Distinct, use un IEqualityComparer. Si siempre desea que los objetos Author se comparen en función de los valores de nombre, anule GetHashCode y Equals, o implemente IEquatable.
Rex M
3
Implementé IEquatable(y anulé Equals/ GetHashCode) pero ninguno de mis puntos de interrupción se activa en estos métodos en un Linq Distinct.
PeterX
2
@PeterX Yo también noté esto. Tuve puntos de interrupción en GetHashCodey Equals, fueron golpeados durante el bucle foreach. Esto se debe a que var temp = books.SelectMany(book => book.Authors).Distinct();devuelve un IEnumerable, lo que significa que la solicitud no se ejecuta de inmediato, solo se ejecuta cuando se utilizan los datos. Si desea un ejemplo de este disparo de inmediato, agregue .ToList()después de .Distinct()y verá los puntos de interrupción en Equalsy GetHashCodeantes de foreach.
JabberwockyDecompiler
70

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

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

Si desea que Author se comporte normalmente como un objeto normal (es decir, solo la igualdad de referencia), pero para los propósitos de Distinct comprobar la igualdad por valores de nombre, use un IEqualityComparer . Si siempre desea que los objetos de Autor se comparen en función de los valores de nombre, anule GetHashCode y Equals , o implemente IEquatable .

Los dos miembros de la IEqualityComparerinterfaz son Equalsy GetHashCode. Su lógica para determinar si dos Authorobjetos son iguales parece ser si las cadenas de nombre y apellido son iguales.

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
fuente
1
¡Gracias! Su implementación GetHashCode () me mostró lo que aún me faltaba. Estaba devolviendo {objeto pasado} .GetHashCode (), no {propiedad que se usa para la comparación} .GetHashCode (). Eso marcó la diferencia y explica por qué el mío seguía fallando: dos referencias diferentes tendrían dos códigos hash diferentes.
pelazem
44

Otra solución sin implementar IEquatable, Equalsy GetHashCodees utilizar la LINQs GroupBymétodo 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);
}
Jehof
fuente
1
me ayudó, solo considerando el rendimiento, ¿funciona esto a la misma velocidad?
Biswajeet
mucho mejor que complicarlo con la implementación de métodos, y si usa EF, delegará el trabajo al servidor SQL.
Zapnologica
Si bien este método puede funcionar, habrá un problema de rendimiento debido a la cantidad de cosas que se agrupan
Bellash
@Bellash Haz que funcione y luego hazlo rápido. Seguro que esta agrupación puede llevar a más trabajo por hacer. pero a veces es engorroso implementar más de lo que desea.
Jehof
2
Prefiero esta solución, pero luego uso un objeto "nuevo" en el grupo por: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong
32

Hay una forma más de obtener valores distintos de la lista de tipos de datos definidos por el usuario:

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

Seguramente, dará un conjunto de datos distinto

Ashu_90
fuente
21

Distinct()realiza la comparación de igualdad predeterminada en objetos en el enumerable. Si no ha anulado Equals()y GetHashCode(), entonces usa la implementación predeterminada en object, que compara referencias.

La solución simple es agregar una implementación correcta de Equals()ya GetHashCode()todas las clases que participan en el gráfico de objetos que está comparando (es decir, Libro y Autor).

La IEqualityComparerinterfaz es una conveniencia que le permite implementar Equals()y GetHashCode()en una clase separada cuando no tiene acceso a las partes internas de las clases que necesita comparar, o si está utilizando un método de comparación diferente.

AndyM
fuente
Muchas gracias por este brillante comentario sobre los objetos participantes.
suhyura
11

Has anulado Equals (), pero asegúrate de anular también GetHashCode ()

Eric King
fuente
+1 para enfatizar GetHashCode (). No agregue la implementación base de HashCode como en<custom>^base.GetHashCode()
Dani
8

¡Las respuestas anteriores son incorrectas! Distinto, como se indica en MSDN, devuelve el Equator predeterminado que, como se indica, la propiedad predeterminada comprueba si el tipo T implementa la interfaz System.IEquatable y, de ser así, devuelve un EqualityComparer que usa esa implementación. De lo contrario, devuelve un EqualityComparer que utiliza las anulaciones de Object.Equals y Object.GetHashCode proporcionadas por T

Lo que significa que siempre que anule Equals, está bien.

La razón por la que su código no funciona es porque marca 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

Alex
fuente
0

Puede usar el método de extensión en la lista que verifica la unicidad según el Hash calculado. También puede cambiar el método de extensión para admitir IEnumerable.

Ejemplo:

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étodo de extensión:

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

Puede lograr esto de dos formas:

1. Puede implementar la interfaz IEquatable como se muestra en el método Enumerable.Distinct o puede ver la respuesta de @ skalb en esta publicación

2. Si su objeto no tiene una clave única, puede usar el método GroupBy para lograr una lista de objetos distintos, que debe agrupar todas las propiedades del objeto y luego seleccionar el primer objeto.

Por ejemplo, como a continuación y trabajando para mí:

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 clase MyObject es como la siguiente:

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 su objeto tiene una clave única, solo puede usarlo en el grupo.

Por ejemplo, la clave única de mi objeto es Id.

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