Oct 26, 2013

Closures em C# - Resposta


No post anterior eu deixei como pergunta qual seria a saída de um programa simples em C#:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
   private static void Main(string[] args)
   {
      foreach (var func in GetFuncs1())
      {
         Console.WriteLine("[first] {0}", func());
      }

      foreach (var func in GetFuncs2())
      {
         Console.WriteLine("[second] {0}", func());
      }
   }

   private static List<Func<int>> GetFuncs1()
   {
      var fs = new List<Func<int>>();

      for (int i = 0; i < 3; i++)
      {
         fs.Add(() => i);
      }
      return fs;
   }

   private static List<Func<int>> GetFuncs2()
   {
      var fs = new List<Func<int>>();

      foreach (var i in Enumerable.Range(0, 3))
      {
         fs.Add(() => i);
      }
      return fs;
    }  
}

A resposta para a pergunta é: depende.

(antes de continuar quero ressaltar que toda vez que me referir a um laço for/foreach, assuma que o mesmo se encontra no contexto de lambda expressions / métodos anônimos que capturam variáveis locais / parâmetros)

Para que você possa enterder o problema melhor é necessário antes entender um pouco sobre closures e a sua relação com o uso de variáveis (locais / parâmetros). Se você precisar refrescar a memória recomendo a leitura deste blog post e também deste outro).

Basicamente ambos os métodos do exemplo (GetFuncs1 e 2) criam lambda expressions as quais em seus corpos referenciam a variável local (i)(linhas 26 e 37); para evitar que a área de memória reservada para tal variável seja reutilizada pela CLR o compilador C# "captura" a variável (você pode utilizar o ILDASM ou ILSpy para verificar o código IL gerado e assim entender melhor este processo) e é aqui que os nossos "problemas" começam.

Observe que eu disse que o compilador "captura" as variáveis, não seus valores no momento da criação das lambda expressions; assim, supondo que tivessemos apenas a chamada ao método GetFuncs1(), o código resultante seria equivalente ao código abaixo (note as linhas marcadas: 20, 25-28 e 30) (o código gerado na realidade é bem diferente mas para nossos propósitos podemos tratá-lo como equivalente):

using System;
using System.Collections.Generic;

class Program
{
 private static void Main(string[] args)
 {
  foreach (var func in GetFuncs1())
  {
   Console.WriteLine("[first] {0}", func());
  }
 }

 private static List<Func<int>> GetFuncs1()
 {
  var fs = new List<Func<int>>();

  for (ret = 0; ret < 3; ret++)
  {
   fs.Add(ret_func);
  }
  return fs;
 }

 private static int ret_func()
 {
  return ret;
 }

 private static int ret;
}
Como vemos, ao invés de criar um método para representar o corpo das lambda expressions em cada iteração do laço, o compilador emitiu um único método estático (ret_func() inserindo referências para o mesmo na lista de funções) que simplesmente retorna o valor do campo ret o qual, por sua vez, é atualizado no laço for!

Agora que você já entende porque a saída do programa foi 3,3,3 restam, pelo menos, duas questões:

  1. Como obter o comportamento desejado, ou seja, que a saída do programa seja 0, 1 e 2 ?
  2. Porque eu disse que a saída do programa "dependia" de algo, e o mais importante, depende do que?
A resposta para a primeira pergunta é sim; (veja o programa abaixo) basta declarar uma variável local dentro do laço for e atribuir a variável "i" para a mesma (linha 20) e, então, retornar o valor desta variável no corpo da lambda expression (linha 21) :
using System;
using System.Collections.Generic;

class Program
{
 private static void Main(string[] args)
 {
  foreach (var func in GetFuncs1())
  {
   Console.WriteLine("[first] {0}", func());
  }
 }

 private static List<Func<int>> GetFuncs1()
 {
  var fs = new List<Func<int>>();

  for (int i = 0; i < 3; i++)
  {
   var capture = i;
   fs.Add(() => capture);
  }
  return fs;
 }
}
Se você executar o programa agora verá que a saída do mesmo é exatamente a esperada; isto ocorre porque, agora, como declaramos uma variável local dentro do laço o compilador irá emitir código equivalente ao código abaixo (efetivamente capturando o valor da variável no momento em que a lambada expression é criada):
using System;
using System.Collections.Generic;

class Program
{
 private static void Main(string[] args)
 {
  foreach (var func in GetFuncs1())
  {
   Console.WriteLine("[first] {0}", func());
  }
 }

 private static List<Func<int>> GetFuncs1()
 {
  var fs = new List<Func<int>>();

  for (int i = 0; i < 3; i++)
  {
   var h = new Holder {value = i};
   fs.Add(h.GetIt);
  }
  return fs;
 }

 class Holder
 {
  public int value;

  public int GetIt()
  {
   return value;
  }
 }
}
Novamente ficou mais simples entender porque este código produz o resultado esperado: em cada iteração do laço o compilador instanciou um objeto para armazenar o valor de "i" naquele momento.

Estou certo de que, de agora em diante, toda vez que você utilizar lambda expressions / métodos anônimos você irá certificar-se de usar a construção compatível com o resultado desejado.

Quanto à segunda questão, se este blog tiver um número razoável de leitores, teremos dois grupos com resultados diferentes: 3,3,3 / 2,2,2 e 3,3,3 / 0,1,2 (que é a saída esperada)!

A diferença nos resultados esta relacionada à versão do compilador C# utilizado.

Veja a imagem abaixo:

A versão 5 do C# mudou o comportamento de variáveis capturadas em laços foreach! A partir desta versão o compilador emite código como se a cada iteração uma nova variável local fosse alocada (o mesmo truque que eu apresentei acima);

Agora eu me questiono porque o comportamento de laços for não foram modificados também. O que posso dizer é que, na minha opinião, esta decisão só introduz confusão. Desenvolvedores iniciantes na linguagem irão, invariavelmente, ser surpreendidos por esta diferença de comportamento.

Caso estes desenvolvedores tenham contato primeiramente com laços for os mesmos ficaram tentando entender porque suas lambdas expressions/métodos anônimos estão observando uma valor atualizado da variável capturada e, então, quando eles entenderem o que esta ocorrendo tenderão a usar o truque de "declarar uma variável local dentro do laço" (inclusive em laços foreach). Por outro lado, caso estes desenvolvedores tenham contato com o laço foreach primeiro, eles correm o risco de em algum momento usar um laço for e introduzir bugs (uma vez que os mesmos assumirão que o valor da variável do laço será capturada).

Há uma discussão sobre o assunto que, ainda que eu compreenda os argumentos, não concordo completamente com os mesmos - partes do meu cérebro ainda acham que esta diferença no comportamento é uma inconsistência gratuíta - mas esta é apenas minha opinião.

Se você estiver interessado em mais detalhes sobre o assunto recomendo a leitura do capítulo 8.8.4 da especificação da linguagem C#.

Finalmente um alerta: se você for portar projetos de uma versão do C# anterior à 5.0 para a versão 5.0 ou mais nova preste atenção na combinação lambda expressions / laços for pois como vimos é possível ocorrer mudanças de comportamento.

O que você acha?

(read this post in english)

No comments: