Acerca de la especificación 5.0 de C#



Métodos anónimos y expresiones lambda


Los métodos anónimos se introdujeron en la versión 2.0 de C# como respuesta a la necesidad de pasar un bloque de código como parámetro de un delegado. Posteriormente (a partir de la versión 3.0 de C#) se introdujeron las expresiones lambda como modo preferente y preferible de inserción de código (no obstante valga decir que los métodos anónimos superan en un punto clave a las expresiones lambda ya que en estos la lista de parámetros es omisible, lo que permite la creación de delegados con diferentes firmas). Para quién desee una explicación más profunda y pormenorizada acerca de delegados, métodos anónimos y expresiones lambda (explicación fuera del alcance del presente artículo), recomiendo la lectura del excelente libro de Jon Skeet “C# in Depth” (C# en Profundidad).


Expresiones lambda y estructuras iterativas foreach


Hace un par de semanas me topé con una entrada un blog en el que se establecía una discusión acerca de un supuesto comportamiento anómalo o indeseado del compilador con respecto al uso de expresiones lambda en el interior de un bucle foreach. 


Analicemos el comportamiento supuestamente anómalo en cuestión:


Actualmente (es decir, en las versiones 3.0 y 4.0 de C#) la variable de bucle en una estructura iterativa de tipo foreach presenta un comportamiento con las expresiones lambda que a ciertos desarrolladores les puede resultar antiintuitivo a primera vista. Veamos un ejemplo:

var numeros = new List<int>() { 1, 2, 3, 4, 5 };
var delegados = new List<Func<int>>();
foreach (var numero in numeros)
{
    delegados.Add(() => numero);
}
foreach (var delegado in delegados)
{
    Console.Write(delegado() + " ");
}
Console.ReadLine();


Analicemos brevemente este fragmento de código. 


En la línea 1 instanciamos una lista fuertemente tipada en la que almacenamos cinco números enteros. En la línea 2 instanciamos una lista (también fuertemente tipada) de delegados genéricos Func. En la línea 3 tenemos un bucle foreach que utilizamos para recorrer los elementos de la lista “numeros” y dado que el tipo subyacente a una expresión lambda es uno de los delegados genéricos de Func, esto nos permite añadir a nuestra lista “delegados” una expresión lambda que va a la variable de bucle en cada iteración, tal y como podemos observar en la línea 5. Utilizamos un segundo bucle en la línea 7 precisamente para recorrer la lista “delegados” y generar una salida por consola del valor al que apunta cada elemento de la lista.


Hagámonos una pregunta: ¿qué salida por pantalla producirá este fragmento de código?


Es perfectamente posible que alguien pueda pensar que la salida por consola debería ser nuestra inicial lista de números:
A. 1 2 3 4 5
Sin embargo, la salida que genera nuestro ejemplo es la siguiente (para desazón de aquellos que pensaron que la salida generada era precisamente la anterior):
B. 5 5 5 5 5
Y ahora la pregunta del millón que se estarán haciendo aquellos lectores que continúen convencidos de que la salida correcta debería ser A y no B: ¿Por qué?


Para llegar a una respuesta, debemos en primer lugar tener claro el comportamiento de la expresión lambda: 
() => numero
Esta función devuelve el valor actual de “numero”, y no el valor de “numero” en el momento en el que se creó el delegado. Obviamente, dado que hemos recorrido la lista completa en el bucle, y el último valor de “numero” es 5, la salida por pantalla que obtenemos es la especificada en B y no la especificada en A como cabría esperar en un primer momento.


Agunos dirán: “buen intento, pero todavía queda la cuestión de fondo sin resolver: ¿por qué no se crea una nueva instancia de la variable de bucle en cada iteración del mismo como cabría esperar?”


Y este punto es precisamente acerca del cual debemos reflexionar: ¿es la creación de una nueva instancia en cada iteración el comportamiento que cabría esperar? De acuerdo a la especificación 4.0 (y anteriores), la respuesta es no. 


La especificación 4.0 del lenguaje: el estándar ECMA-334


El quid de la cuestión reside en el modo cómo el compilador expande el bloque de código correspondiente al bucle foreach. Esta expansión está especificada en la versión 4 de la especificación del lenguaje C# (estándar ECMA-334), (concretamente en la página 238). A partir de la misma, la sentencia iterativa expandiría de un modo similar al fragmento siguiente:

IEnumerator<int> e = ((IEnumerable<int>)(numeros)).GetEnumerator();
try
{
    int numero;
    while (e.MoveNext())
    {
        numero = (int)(int)e.Current;
        delegados.Add(() => numero);
    }
}
finally
{
    ((IDisposable)e).Dispose();
}


Tal y como podemos apreciar en este fragmento de código (equivalente al fragmento comprendido entre las líneas 3 y 5 del ejemplo anterior), la variable “numero” está declarada fuera del bucle while, y precisamente ahí es dónde reside el problema, ya que sólo el valor de ésta es nuevo (es, tal y como se puede apreciar, la misma instancia) lo que combinado con las características propias de la clausura, provocará las consecuencias ya explicadas en los párrafos anteriores (nuestra “inesperada” salida B por consola).


Así pues, si lo que deseamos obtener con este fragmento de código la lista completa de elementos en “números” (A) debemos modificar nuestro código apoyándonos en una variable auxiliar tal y como se puede apreciar en las líneas 5 y 6 del siguiente fragmento de código:

var numeros = new List<int>() { 1, 2, 3, 4, 5 };
var delegados = new List<Func<int>>();
foreach (var numero in numeros)
{
    var numero2 = numero;
    delegados.Add(() => numero2);
}
foreach (var delegado in delegados)
{
    Console.Write(delegado() + " ");
}
Console.ReadLine();


Modificaciones en la versión 5.0 de la especificación del lenguaje


Tal y como comentábamos anteriormente, esto puede resultar un poco antiintuitivo, por lo que el equipo de Microsoft responsable del compilador de C# ha tomado la decisión de modificar la especificación del lenguaje en su versión 5 de modo que no sea necesario declarar una variable auxiliar para que el fragmento de código 1 produzca la salida B. 


Esto se consigue modificando el modo del que el compilador expande el bucle foreach. A partir de la versión 5, la expansión del fragmento de código 1 será la siguiente:

IEnumerator<int> e = ((IEnumerable<int>)(numeros)).GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int numero = (int)(int)e.Current;
        delegados.Add(() => numero);
    }
}
finally
{
    ((IDisposable)e).Dispose();
}

De este modo, como podemos apreciar en el fragmento de código anterior, “numero” se encontraría a nivel lógico en el interior de la estructura iterativa while, lo que produciría la salida B y no la A.


Conclusiones


Lo cierto es que la decisión aunque más allá de las clausuras, no tiene ningún efecto semántico, no es trivial debido a las siguientes razones:


El comportamiento de los bucles foreach con las clausuras será fundamentalmente inconsistente entre diferentes versiones del compilador empleadas para generar los ensamblados (es decir: el mismo código compilado se comportará de modos diferentes dependiendo de la versión del compilador utilizada para generar el ensamblado).

La especificación del bucle for será inconsistente con respecto a la especificación del bucle foreach.

Una declaración que a nivel léxico es exterior a la estructura iterativa se trata a nivel lógico como si estuviese en el interior de la misma.


Sin embargo la ventaja fundamental, en mi opinión, es que el resultado final es quizá más asequible e intuitivo para el conjunto global de desarrolladores de C#.

Comentarios

  1. Buen artículo Mario. No sabía que cambiaba el comportamiento de foreach en la 5 entrega de c#. Está bien saberlo :)

    ResponderEliminar

Publicar un comentario

Entradas populares