Cómo se compila una expresión lambda en C#

Hasta hace poco siempre había pensado que en C# las expresiones lambda se compilaban siempre como clases, pero charlando en twitter sobre cómo se compilan en Java 8, @__josejuan__ puso un ejemplo en el que se veía claramente que se compilaban como métodos de la clase que las contiene:

lambdas

Al principio pensé que era algo relativo a la versión del framework, pero como esa explicación no me acababa de cuadrar, no he podido resistirme a investigar un poco más.

OJO: Como se explica en la especificación de C# (sección 6.5.2) todo esto es un detalle de implementación, por lo que podría cambiar en futuras versiones.

Para decidir cómo compilar una expresión lambda, el compilador distingue dos casos:

  • Funciones que no usan ninguna variable externa a la función.
  • Funciones que usan variables externas a la función.

Funciones que no dependen de variables externas

El primer caso sería una función como ésta:

public static void Main(string[] args)
{
  Func<int, int, int> f = (a,b) => a + b;

  Console.WriteLine(f(2, 3));
}

En este caso, la función f no depende de ninguna variable externa a la función, sólo depende de sus parámetros: a y b. Por ello, es posible representarla como un método estático en la clase en que está declarada y eso es lo que hace el compilador de C#, genera un método estático parecido a esto:

[CompilerGenerated]
private static int <Main>__0(int a, int b)
{
  return a + b;
}

Al tener este método, se puede reescribir el código original como:

public static void Main(string[] args)
{
  var f = new Func<int, int, int>(<Main>__0);

  Console.WriteLine(f(2, 3));
}

Funciones que usan variables externas

Si la función utiliza variables externas a ella, tendríamos un caso como éste:

public static void Main(string[] args)
{
  var someValue = 17;

  Func<int, int> g = a => someValue + a;
  Console.WriteLine(g(3));
}

En este caso la función depende de un valor externo a ella (someValue), por lo que no se puede aplicar la misma técnica que en el ejemplo anterior y lo que hace el compilador es generar una clase más o menos así:

[CompilerGenerated]
private sealed class <>__DisplayClass3
{
  public int someValue;

  public int <>b__1(int a)
  {
    return this.someValue+ a;
  }
}

La clase mantiene referencias a aquellas variables externas que son usadas por la función, de manera que luego se puedan utilizar al “invocar” la función a través del método <>b__1. Con esto se puede reescribir el código original como:

public static void Main(string[] args)
{
  var someValue = 17;

  var g = new <>__DisplayClass3();
  g.someValue = someValue;

  Console.WriteLine(g.<>b__1(3))
}

Resumen

Aunque la técnica de crear clases es lo suficientemente flexible como para cubrir los dos casos de uso, el compilador de C# es bastante avanzado y es capaz de optimizar el código que genera, evitando crear clases si no es necesario.

Y recuerda que esto es un detalle de implementación que podría cambiar en futuras versiones del framework o del compilador, por lo que no deberías basarte en este comportamiento para nada.

3 comentarios en “Cómo se compila una expresión lambda en C#

  1. Es curioso compararlo con la opcion que han escogido en la jdk 8: crear un metodo no estatico asociado a la clase (con el nuevo invoke dynamic) donde es definida la funcion anonima para poder clausurar las variables externas.
    Contrasta tambien con la solucion de casi todos los lenguajes de la jvm, que es crear siempre una clase parametrizada (como scala) o no (como groovy).
    Supongo que el motivo habra sido la eficiencia (crear demasiadas clases anonimas suele dar problemas con el gc) y la bendita compatibilidad hacia atras.

  2. ¿Cómo hace la captura de variables locales? ¿Las convierte en variables de instancia para que sean accesibles al método? ¿Las pasa como parametro? ¿Las copia en el cuerpo del método?

  3. Supongo que preguntas por java 8:
    Citando a http://doanduyhai.wordpress.com/2012/07/12/java-8-lambda-in-details-part-ii-scoping-of-this-and-effectively-final-variable-semantic/
    «Unlike anonymous functions, a lambda expression can be considered as a simple code block with regarde to variable scoping. Consequently all scoping rules for code blocks also apply to lambda expression.»
    Eso quiere decir (como explica el articulo mejor que yo) que usa ambito lexico y no crea un nuevo nivel de ambito por lo que los parametros de la expresion lambda no pueden tener el mismo nombre que las variables locales donde se usa/declara.
    Ademas las variables locales tienen que ser efectivamente finales (o declaradas como finales o no modificadas antes de la expresion lambda).
    Lo primero es un poco deficiente o extraño, lo segundo lo veo como una ventaja.
    De las opciones de tu pregunta lo mas aproximado creo que (no se como es la implementacion interna) seria que las copia en el cuerpo del metodo que representa a la expresion lambda.

Comentarios cerrados.