Mostrando postagens com marcador C#. Mostrar todas as postagens
Mostrando postagens com marcador C#. Mostrar todas as postagens

segunda-feira, 4 de julho de 2011

Linq, Distinct(), IEqualityComparer e GetHashCode()

Disclaimer: esse artigo é sobre um detalhe tão específico do Framework, que vale mais pelas fotos do que pelo conteúdo. Mas se você for uma das 19 pessoas no mundo que querem usar Distinct() em Linq para evitar instâncias repetidas no seu resultado, além das belas paisagens o código pode te ajudar.
GB no Big BenEstou eu encalhado aqui no Galeão, voltando de uma semana nas Európias – eu e Minha Dona (letra maiúscula que não sou doido) fomos conhecer Paris e Londres. Nosso vôo pra Brasília só sai daqui a 6 horas; eu não tô fazendo nada mesmo, então pensei em escrever um pouco sobre Linq, Distinct(), IEqualityComparer e GetHashCode().
Uma aplicação nossa lê de um arquivo um monte de registros de produtos pra dentro de uma List<Produtos>. Aí eu fui jogar estes produtos na tabela de Produtos no BD, mas como havia registros repetidos no arquivo, apareceram vários registros para o mesmo produto no banco.
“Bem, é só fazer o DISTINCT na lista”. Ok. Fui olhar o método Distinct() do Linq e achei duas assinaturas: uma recebe uma expressão lambda cujo resultado, ao ser avaliado para cada registro da lista, define os registros distintos; e outra recebe uma instância de IEqualityComparer, a qual nem dei uma segunda olhada.
Pra gerar minha lista de produtos distintos, escrevi:
// 1ª tentativa: Distinct(p => p) pra retornar uma lista de produtos sem repetidos 
List<Produto> listaSemRepetidos = listaProdutos.Distinct(p => p);
Vênus de Milo no Louvre - Grandes MiércolesEstranhamente, isto fez com que *todos* os objetos em listaProdutos aparececem em listaSemRepetidos. Pensando melhor, faz sentido. Distinct() tem que ver se os objetos são iguais para decidir se eles são repetidos, então deve chamar Equals() – qualquer classe tem Equals(), que é herdado de System.Object - e na implementação default de Equals() dois objetos são iguais se apontam para a mesma instância, e não se seus campos contem o mesmo valor.
Antes de ir escovar bit e partir pra uma implementação customizada de Equals() na minha classe Produtos, resolvi dar uma olhada com mais calma na segunda assinatura do Distinct(), aquela que recebe uma instância de IEqualityComparer. Esta interface contem dois métodos: bool Equals(<T>, <T>) e int GetHashCode(). “Morreu”, pensei eu. “É só implementar a interface e na implementação do Equals(<T>,<T>), comparar os campos das instâncias recebidas”. Minha implementação ficou assim:
public class ComparadorProdutos : IEqualityComparer<AcessoDados.Entidades.Produto>
{ 
    public bool Equals(AcessoDados.Entidades.Produto p1, AcessoDados.Entidades.Produto p2) 
    {
        // Trata os casos nos quais há objetos nulos 
        if(p1 == null && p2 == null) return true;
        if(p1 == null && p2 != null) return false;
        if(p1 != null && p2 == null) return false;
        // Se nenhum dos dois é nulo, retorna true se os campos “importantes” contém o mesmo valor
        return 
        (
            p1.CodigoProduto == p2.CodigoProduto &&
            p1.DataInicial == p2.DataInicial &&
            p1.DataFinal == p2.DataFinal &&
            p1.CodigoNCM == p2.CodigoNCM &&
            p1.Descricao == p2.Descricao &&
            p1.UnidadeMedida == p2.UnidadeMedida &&  
            p1.AliquotaICMS == p2.AliquotaICMS && 
            p1.AliquotaIPI == p2.AliquotaIPI &&
            p1.ReducaoBaseCalculoICMS == p2.ReducaoBaseCalculoICMS &&
            p1.BaseCalculoICMSSubstTrib == p2.BaseCalculoICMSSubstTrib &&
            p1.IdCliente == p2.IdCliente  
        );
    }

    // Retorna qualquer coisa em GetHashCode() pq não vou usar esse trem mesmo
    public int GetHashCode(AcessoDados.Entidades.Produto p)
    {
        if(p == null) return -1;
        else return p.GetHashCode();
    }
}
Na implementação do GetHashCode() coloquei qualquer leseira, porque o que o Distinct() ia usar mesmo era o Equals(). Testei com um programinha console, tudo ok, agora é só chamar o Distinct() passando uma instância de ComparadorProdutos.
// 2ª tentativa: Passando uma implementação de IEqualityComparer para Distinct() 
List<Produto> listaSemRepetidos = listaProdutos.Distinct(new ComparadorProdutos());
Torre de Londres - Show!!!E de novo voltaram *todos* os registros. Caraca. Testei de um lado, do outro, volta pra aplicaçãozinha console pra ver se o Equals() não tem alguma falha, e tudo ok. Só não funciona. Finalmente entreguei os pontos e fui consultar A Fonte De Toda A Sabedoria. E realmente encontrei um artigo “LINQ: Distinct() does not work as expected”, de outro sofredor que passou pela mesma novela. Mas antes de recorrer ao Google, ele colocou um breakpoint em cada um dos métodos da sua implementação de IEqualityComparer e viu que, em vez de chamar Equals(), o método Distinct() chama GetHashCode() para verificar se os objetos recebidos são iguais!!! Se os objetos tem o mesmo valor para GetHashCode() então eles são considerados iguais, e é assim que o Distinct() separa objetos "repetidos" e "únicos". Ele até cita exemplos de boas implementações para o GetHashCode(), mas a melhor dica quanto a isto é que o próprio .NET Framework já fornece uma boa implementação: a usada para tipos anônimos. Então na classe ComparadorProdutos mudei a minha implementação do GetHashCode() para retornar o hash code de um tipo anônimo com os campos da classe Produto que eu queria comparar:
    public int GetHashCode(AcessoDados.Entidades.Produto p) 
    {
        if(p == null) 
            return 0;
        else 
            return new 
            {
                p.CodigoProduto,
                p.DataInicial,
                p.DataFinal,
                p.CodigoNCM,
                p.Descricao,
                p.UnidadeMedida,
                p.AliquotaICMS,
                p.AliquotaIPI,
                p.ReducaoBaseCalculoICMS,
                p.BaseCalculoICMSSubstTrib,
                p.IdCliente 
            }.GetHashCode();
    }
Se duas instâncias da classe Produto tem os mesmos valores nestes campos, seus hash codes serão iguais. E aí meu Distinct(new ComparadorProdutos) finalmente funcionou.

Cris me ajudando na redação do post no Galeão
Cris me ajudando na redação do post no Galeão

É, nada como 6 horas de aeroporto pra gastar…
[]s,
GB

sexta-feira, 8 de outubro de 2010

Como passar lista de objetos de classe derivada para método que recebe lista objetos de classe base

Se você tem um método que recebe um parâmetro List<T>, e quer passar para este método uma lista List<T1>, aonde T1 é derivada de T, então continue lendo. (Se não, pode voltar ao que você estava fazendo ;-)

Criei o seguinte método:
public static void IncluiEntidades(List<EntidadeAzureTable> entidades)
{
...
}
EntidadeAzureTable é a classe base de várias outras na aplicação. Este método recebe uma lista de entidades e as grava nas respectivas tabelas (no Azure Table Service).

Pois bem, criei uma lista de objetos de uma classe derivada e tentei passá-la para o método acima:

List<DocumentoSaida> documentos; // DocumentoSaida deriva de EntidadeAzureTable
documentos = ...;
AzureTables.IncluiEntidades(documentos);
O compilador do C# não deixou passar, retornando o seguinte erro de compilação:

Argument 1: cannot convert from 'System.Collections.Generic.List<DocumentoSaida>' to 'System.Collections.Generic.List<EntidadeAzureTable>'

Ele “não entendeu” que DocumentoSaida herda de EntidadeAzureTable. Aparentemente, se um método recebe um parâmetro que é uma lista de objetos de uma classe, não posso passar uma lista de objetos de uma classe derivada neste parâmetro.

Para resolver este problema, podemos utilizar generic type constraints: criar um método genérico, e usar a palavra-chave where para colocar uma restrição no parâmetro de tipo do método. Mudei a declaração do método para

public static void IncluiEntidades<T>(List<T> entidades)
     where T : EntidadeAzureTable
{
   ...
}
e o compilador entendeu o que eu estava querendo fazer.

sábado, 17 de julho de 2010

Curto-circuito no C#!!!

A seguir está um código que recebe duas strings, e se a conversão de ambas para valores int for feita com sucesso, realiza um processamento qualquer. Se qualquer uma das conversões falhar, o processamento não é executado, e a conversão inválida deve ser escrita em um log de erros.


int int1 = -1, int2 = -1;
if (int.TryParse(string1, out int1) && int.TryParse(string2, out int2))
{
// Conversões ok - usa os valores em int1 e int2
}
else
{
// Pelos menos uma das conversões falhou - loga o erro
if(int1 == -1) LogaErro("Erro ao converter string1");
if(int2 == -1) LogaErro("Erro ao converter string2");
}


Testei o código com string1 = "XX" e string2 = "000001". E o erro que apareceu no log foi "Erro ao converter string2"!!!


[...Tire alguns momentos para revisar o código e tentar ver o que deu errado...]


O problema é que C# realiza a avaliação de uma expressão booleana em "curto-circuito" (shor-circuit boolean evaluation), ou seja, a expressão é executada até o ponto no qual se conheça seu resultado; os "pedaços" restantes da expressão não são executados. Se string1 = "XX" e string2 = "00001", ao executar a linha

if (int.TryParse(string1, out int1) && int.TryParse(string2, out int2))

o primeiro int.TryParse() falha, já que "XX" não é um número válido. E duas coisas acontecem:

Coisa 1: Como o primeiro int.TryParse() retornou falso, o resultado da expressão já é conhecido, pois (falso && qualquer coisa) é falso. Então o segundo int.TryParse() não roda.

Coisa 2: Quando int.TryParse() falha, ele coloca 0 (zero) como valor do inteiro que armazenaria o resultado da conversão (o segundo parâmetro).

Como resultado da coisa 1, int2 permance com o valor inicial -1. Como resultado da coisa 2, int1 recebe 0. Então quando o else é executado, o programa entende que a conversão de string1 (valor "XX") deu certo, enquanto que a conversão de string2 (valor "000001") deu errado! De forma geral, devem ser evitadas na expressão booleana de um if chamadas que modificam dados, pois como C# usa avaliação booleana em curto-cirtuito, pode ser que nem todas as partes da expressão sejam avaliadas, o que podem causar resultados imprevisíveis no código.

Para corrigir o código inicial, devemos então fazer as chamadas a int.Parse() antes do if:


int int1, int2;
bool conversao1ok = int.TryParse(string1, out int1);
bool conversao2ok = int.TryParse(string2, out int2);
if (conversao1ok && conversao2ok)
{
// Conversões ok - usa os valores em int1 e int2
}
else
{
// Pelos menos uma das conversões falhou - loga o erro
if(!conversao1ok) LogaErro("Erro ao converter string1");
if(!conversao2ok) LogaErro("Erro ao converter string2");
}