Distinto, não funcionando com 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;
    }

}

Isso se baseia em um exemplo em "LINQ em ação". Listagem 4.16.

Isso imprime Jon Skeet duas vezes. Por quê? Eu até tentei substituir o método Equals na classe Author. Ainda Distinto não parece funcionar. o que estou perdendo?

Editar: Eu adicionei == e! = Sobrecarga de operador também. Ainda sem ajuda.

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

Respostas:

159

LINQ Distinct não é tão inteligente quando se trata de objetos personalizados.

Tudo o que ele faz é olhar para sua lista e ver que ela possui dois objetos diferentes (não importa se eles têm os mesmos valores para os campos de membro).

Uma solução alternativa é implementar a interface IEquatable conforme mostrado aqui .

Se você modificar sua classe Author dessa forma, ela deve 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;
    }
}

Experimente como DotNetFiddle

Skalb
fonte
22
IEquatable está bem, mas está incompleto; você deve sempre implementar Object.Equals () e Object.GetHashCode () juntos; IEquatable <T> .Equals não substitui Object.Equals, portanto, isso falhará ao fazer comparações não fortemente tipadas, o que ocorre frequentemente em estruturas e sempre em coleções não genéricas.
AndyM,
Portanto, é melhor usar a substituição de Distinct que leva IEqualityComparer <T> como Rex M sugeriu? Quero dizer o que devo fazer se não quiser cair na armadilha.
Tanmoy,
3
@Tanmoy isso depende. Se você deseja que Author normalmente se comporte como um objeto normal (ou seja, apenas referência de igualdade), mas verifique os valores de nome para fins de Distinct, use um IEqualityComparer. Se você sempre deseja que os objetos Author sejam comparados com base nos valores de nome, substitua GetHashCode e Equals ou implemente IEquatable.
Rex M,
3
Eu implementei IEquatable(e substituí Equals/ GetHashCode), mas nenhum dos meus pontos de interrupção está disparando nesses métodos em um Linq Distinct?
PeterX
2
@PeterX Eu também percebi isso. Tive pontos de interrupção no GetHashCodee Equals, eles foram atingidos durante o loop foreach. Isso porque o var temp = books.SelectMany(book => book.Authors).Distinct();retorna um IEnumerable, significando que a solicitação não é executada imediatamente, ela só é executada quando os dados são utilizados. Se você quiser um exemplo desse disparo imediatamente, adicione .ToList()após o .Distinct()e verá os pontos de interrupção no Equalse GetHashCodeantes do foreach.
JabberwockyDecompiler
70

O Distinct()método verifica a igualdade de referência para tipos de referência. Isso significa que ele está procurando literalmente o mesmo objeto duplicado, não objetos diferentes que contêm os mesmos valores.

Há uma sobrecarga que leva um IEqualityComparer , portanto, você pode especificar uma lógica diferente para determinar se um determinado objeto é igual a outro.

Se você deseja que Author normalmente se comporte como um objeto normal (ou seja, apenas referência de igualdade), mas para fins de verificação de igualdade distinta por valores de nome, use um IEqualityComparer . Se você sempre deseja que os objetos Author sejam comparados com base nos valores de nome, substitua GetHashCode e Equals ou implemente IEquatable .

Os dois membros da IEqualityComparerinterface são Equalse GetHashCode. Sua lógica para determinar se dois Authorobjetos são iguais parece ser se as strings de nome e sobrenome forem iguais.

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
fonte
1
Obrigado! Sua implementação GetHashCode () me mostrou o que ainda estava faltando. Eu estava retornando {objeto passado} .GetHashCode (), não {propriedade sendo usada para comparação} .GetHashCode (). Isso fez a diferença e explica por que o meu ainda estava falhando - duas referências diferentes teriam dois códigos hash diferentes.
pelazem
44

Outra solução sem implementação IEquatable, Equalse GetHashCodeé usar o GroupBymétodo LINQs e selecionar o primeiro item do 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
fonte
1
isso me ajudou, apenas considerando o desempenho, ele funciona na mesma velocidade?
Biswajeet
muito mais agradável do que complicá-lo com a implementação de métodos, e se usar EF irá delegar o trabalho ao servidor sql.
Zapnologica
embora este método possa funcionar, haverá um problema de desempenho devido ao número de itens sendo agrupados
Bellash
@Bellash Faça funcionar e, depois, rápido. Claro que esse agrupamento pode levar a mais trabalho a ser feito. mas às vezes é complicado implementar mais do que você deseja.
Jehof
2
Eu prefiro esta solução, mas usando um objeto "novo" no grupo: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong
32

Há mais uma maneira de obter valores distintos da lista de tipos de dados definidos pelo usuário:

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

Certamente, fornecerá um conjunto distinto de dados

Ashu_90
fonte
21

Distinct()executa a comparação de igualdade padrão em objetos no enumerável. Se você não substituiu Equals()e GetHashCode(), ele usa a implementação padrão em object, que compara as referências.

A solução simples é adicionar uma implementação correta de Equals()e GetHashCode()para todas as classes que participam do gráfico de objeto que você está comparando (isto é, Livro e Autor).

A IEqualityComparerinterface é uma conveniência que permite a você implementar Equals()e GetHashCode()em uma classe separada quando você não tem acesso aos detalhes internos das classes que precisa comparar, ou se estiver usando um método diferente de comparação.

AndyM
fonte
Muito obrigado por este comentário brilhante sobre os objetos participantes.
suhyura
11

Você substituiu Equals (), mas certifique-se de também substituir GetHashCode ()

Eric King
fonte
1 para enfatizar GetHashCode (). Não adicione a implementação de HashCode base como em<custom>^base.GetHashCode()
Dani
8

As respostas acima estão erradas !!! Distinto, conforme declarado no MSDN, retorna o Equador padrão que, conforme declarado, A propriedade Default verifica se o tipo T implementa a interface System.IEquatable e, em caso afirmativo, retorna um EqualityComparer que usa essa implementação. Caso contrário, ele retorna um EqualityComparer que usa as substituições de Object.Equals e Object.GetHashCode fornecidos por T

O que significa que enquanto você ignorar Igual a você está bem.

O motivo pelo qual seu código não está funcionando é porque você verificou firstname == lastname.

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

Alex
fonte
0

Você pode usar o método de extensão na lista que verifica a exclusividade com base no Hash calculado. Você também pode alterar o método de extensão para oferecer suporte a IEnumerable.

Exemplo:

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 extensão:

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

Você pode conseguir isso de duas maneiras:

1. Você pode implementar a interface IEquatable como mostrado pelo Método Enumerable.Distinct ou pode ver a resposta de @ skalb neste post

2. Se o seu objeto não possui uma chave única, você pode usar o método GroupBy para obter uma lista de objetos distintos, que deve agrupar todas as propriedades do objeto e após selecionar o primeiro objeto.

Por exemplo, como abaixo e trabalhando para mim:

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

A classe MyObject é como a seguir:

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 o seu objeto tiver uma chave exclusiva, você só poderá usá-lo no grupo por.

Por exemplo, a chave exclusiva do meu objeto é Id.

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