Arquivo de abril \09\UTC 2011

C#, Closures e Resharper

Olá pessoal! Tivemos um problema esta semana, em tese de origem simples, mas que nos tomou um tempo enorme de debug. Quero compartilhar aqui para tentar evitar que outros sigam pelo mesmo caminho!

Para começar, imaginem este código ilustrativo abaixo. Uma rotina simples para enumerar duas vezes por uma coleção e retornar a mesma, modificada. Claro que isto é somente representativo de um código real, mas imaginem qualquer função que por alguma razão modifica elementos de uma lista e retorna a mesma ou um subconjunto da mesma. Trivial, certo? O resultado esperado desta execução são duas linhas com o número “2”.

class MinhaClase {
    public decimal Valor { get; set; }
}
    
class Program {
    static IList<MinhaClase> Calcular(IList<MinhaClase> classes) {
        foreach (var minhaClase in classes) {
            minhaClase.Valor = 1;
        }
        foreach (var minhaClase in classes) {
            minhaClase.Valor++;
        }
        return classes;
    }
    static void Main(string[] args) {
        var classes = new[] {
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 1}
                            };
        var result = Calcular(classes.Where(c => c.Valor != 1).ToList());
        foreach (var minhaClase in result) {
                Console.WriteLine(minhaClase.Valor.ToString());
        }
        Console.ReadLine();
    }
}

Todo o problema começa quando a gente tenta refatorar e melhorar o código. Como todo bom desenvolvedor, nós usamos o Resharper, que é uma ferramenta fantástica da JetBrains para nos ajudar a gerar um bom código. Na White Fox, nós a usamos praticamente todo o tempo, ela nos poupa muito esforço de código e ajuda a evitar problemas graves de maneira completamente automática. Porém, neste caso, como a rotina “Calcular” simplesmente enumera, o Resharper vai sugerir que a gente mude o IList<> para sua classe base, um IEnumerable<>. A regra seguida por ele faz sentido pois se só estamos usando métodos da classe base, não seria necessário usar a classe derivada na assinatura. O código gerado então após a refatoração fica da seguinte forma:

class MinhaClase {
    public decimal Valor { get; set; }
}
    
class Program {
    static IEnumerable<MinhaClase> Calcular(IEnumerable<MinhaClase> classes) {
        foreach (var minhaClase in classes) {
            minhaClase.Valor = 1;
        }
        foreach (var minhaClase in classes) {
            minhaClase.Valor++;
        }
        return classes;
    }
    static void Main(string[] args) {
        var classes = new[] {
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 1}
                            };
        var result = Calcular(classes.Where(c => c.Valor != 1));
        foreach (var minhaClase in result) {
                Console.WriteLine(minhaClase.Valor.ToString());
        }
        Console.ReadLine();
    }
}

Depois deste refactor, o resultado do programa não deveria mudar, certo? Errado! Os mais experientes irão saber que o resultado da execução deste segundo código é uma lista vazia!

A explicação para isto tem a ver com o suporte do C# para closures (ver este post, que explica o que são de maneira simples, ou esta explicação mais acadêmica aqui). As operações LINQ simplesmente geram predicados para uma closure que é executada no momento que a coleção é acessada. Tem a ver também com a diferença básica entre um IList<> e um IEnumerable<>, já que num IList<>, um GetEnumerator (executado em um foreach) sempre aponta pra coleção interna, já existente… Já em um IEnumerable<>, o GetEnumerator vai sempre acionar o container para criar coleção e aí enumerar os elementos… Isto significa que a coleção de um IEnumerable<> produzido por uma closure vai ser gerada A CADA foreach, e não uma única vez. Ou seja, cada foreach da rotina “Calcular” executa a closure, bem como o foreach do resultado final. Como neste caso a closure é um filtro de Valor != 1, no segundo foreach da rotina “Calcular” e no último (onde ele imprime o resultado no console) ele simplesmente vai retornar 0 elementos!

Onde erramos? Os mais experientes vão notar que erramos na hora que tiramos o .ToList() do final da closure que define o filtro. A razão da remoção foi que como a rotina “Calcular” agora aceita um IEnumerable<>, o ToList() não seria mais necessário. Sem o ToList(), a closure não é mais executada naquele momento e sim passada para a variável e aí toda a rotina Calcular para de funcionar. Basta colocar o .ToList() de volta que tudo passa a funcionar novamente. Este erro, aparentemente óbvio, na prática é muito fácil de ser cometido, pois o uso de expressões LINQ é cada vez mais comum e, em um código complexo de negócios, se a função sendo chamada tem parametros IEnumerable<>, uma closure pode acabar sendo enviada inadivertidamente.

Portanto, neste caso, o Resharper não só facilita os erros, como pode ainda produzir alguns muito difíceis de identificar (no nosso casso, foram horas gastas até achar onde estava a closure sendo passada). Claro que desligamos esta sugestão na nossa configuração do Resharper e o problema foi resolvido. É importante ressaltar que, mesmo apesar deste caso, continuamos a achar o Resharper uma ferramenta essencial de trabalho para nós.

Outro ponto que merece ser comentado, é que pesquisando este problema, entendemos melhor o funcionamento do IEnumerable<> e o IList<>. Como o IEnumerable<> sempre tem que gerar a coleção a cada GetEnumerator(), podemos ter um desempenho pior das coleções, na maioria dos cenários, comparado a um IList<>. Só isto já seria motivo suficiente para favorecer o uso do IList<> ao invés do IEnumerable<>.

Bom, espero ter ajudado a poupar o tempo de outras pessoas ao deparar com problemas similares. Por causa deste problema, acabamos tendo que conhecer bem mais sobre closures e coleções no .NET, assim, no final, a existência deste bug até que foi benéfica!

Até a próxima!

, ,

4 Comentários