TL; DR : son ejemplos equivalentes en la capa IL.
DotNetFiddle hace que esto sea bonito de responder ya que le permite ver la IL resultante.
Utilicé una variación ligeramente diferente de su construcción de bucle para acelerar mis pruebas. Solía:
Variación 1:
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello World");
int x;
int i;
for(x=0; x<=2; x++)
{
i = x;
Console.WriteLine(i);
}
}
}
Variación 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
En ambos casos, la salida IL compilada hizo lo mismo.
.class public auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method public hidebysig static void Main() cil managed
{
//
.maxstack 2
.locals init (int32 V_0,
int32 V_1,
bool V_2)
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ldc.i4.0
IL_000d: stloc.0
IL_000e: br.s IL_001f
IL_0010: nop
IL_0011: ldloc.0
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: call void [mscorlib]System.Console::WriteLine(int32)
IL_0019: nop
IL_001a: nop
IL_001b: ldloc.0
IL_001c: ldc.i4.1
IL_001d: add
IL_001e: stloc.0
IL_001f: ldloc.0
IL_0020: ldc.i4.2
IL_0021: cgt
IL_0023: ldc.i4.0
IL_0024: ceq
IL_0026: stloc.2
IL_0027: ldloc.2
IL_0028: brtrue.s IL_0010
IL_002a: ret
} // end of method Program::Main
Entonces, para responder a su pregunta: el compilador optimiza la declaración de la variable y hace que las dos variaciones sean equivalentes.
Según tengo entendido, el compilador .NET IL mueve todas las declaraciones de variables al comienzo de la función, pero no pude encontrar una buena fuente que estableciera claramente que 2 . En este ejemplo en particular, verá que los movió con esta declaración:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
En donde nos volvemos demasiado obsesivos al hacer comparaciones ...
Caso A, ¿todas las variables se mueven hacia arriba?
Para profundizar un poco más en esto, probé la siguiente función:
public static void Main()
{
Console.WriteLine("Hello World");
int x=5;
if (x % 2==0)
{
int i = x;
Console.WriteLine(i);
}
else
{
string j = x.ToString();
Console.WriteLine(j);
}
}
La diferencia aquí es que declaramos un int i
o un string j
basado en la comparación. Nuevamente, el compilador mueve todas las variables locales a la parte superior de la función 2 con:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Me pareció interesante observar que, aunque int i
no se declarará en este ejemplo, el código para admitirlo todavía se genera.
Caso B: ¿Qué pasa en foreach
lugar de for
?
Se señaló que foreach
tiene un comportamiento diferente for
y que no estaba comprobando lo mismo por lo que me habían preguntado. Así que puse estas dos secciones de código para comparar la IL resultante.
int
declaración fuera del bucle:
Console.WriteLine("Hello World");
List<int> things = new List<int>(){1, 2, 3, 4, 5};
int i;
foreach(var thing in things)
{
i = thing;
Console.WriteLine(i);
}
int
declaración dentro del bucle:
Console.WriteLine("Hello World");
List<int> things = new List<int>(){1, 2, 3, 4, 5};
foreach(var thing in things)
{
int i = thing;
Console.WriteLine(i);
}
La IL resultante con el foreach
bucle fue de hecho diferente de la IL generada usando el for
bucle. Específicamente, el bloque init y la sección del bucle cambiaron.
.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
int32 V_1,
int32 V_2,
class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
bool V_5)
...
.try
{
IL_0045: br.s IL_005a
IL_0047: ldloca.s V_4
IL_0049: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_004e: stloc.1
IL_004f: nop
IL_0050: ldloc.1
IL_0051: stloc.2
IL_0052: ldloc.2
IL_0053: call void [mscorlib]System.Console::WriteLine(int32)
IL_0058: nop
IL_0059: nop
IL_005a: ldloca.s V_4
IL_005c: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0061: stloc.s V_5
IL_0063: ldloc.s V_5
IL_0065: brtrue.s IL_0047
IL_0067: leave.s IL_0078
} // end .try
finally
{
IL_0069: ldloca.s V_4
IL_006b: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0071: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0076: nop
IL_0077: endfinally
} // end handler
El foreach
enfoque generó más variables locales y requirió algunas ramificaciones adicionales. Esencialmente, la primera vez que salta al final del bucle para obtener la primera iteración de la enumeración y luego salta a casi la parte superior del bucle para ejecutar el código del bucle. Luego continúa girando como cabría esperar.
Pero más allá de las diferencias de ramificación causadas por el uso de las construcciones for
y foreach
, no hubo diferencias en la IL según el lugar donde int i
se colocó la declaración. Entonces todavía estamos en los dos enfoques que son equivalentes.
Caso C: ¿Qué pasa con las diferentes versiones del compilador?
En un comentario que quedó 1 , había un enlace a una pregunta de SO con respecto a una advertencia sobre el acceso variable con foreach y el uso del cierre . La parte que realmente me llamó la atención en esa pregunta fue que puede haber diferencias en cómo funcionaba el compilador .NET 4.5 en comparación con versiones anteriores del compilador.
Y ahí es donde el sitio DotNetFiddler me decepcionó: todo lo que tenían disponible era .NET 4.5 y una versión del compilador de Roslyn. Así que saqué una instancia local de Visual Studio y comencé a probar el código. Para asegurarme de que estaba comparando las mismas cosas, comparé el código construido localmente en .NET 4.5 con el código DotNetFiddler.
La única diferencia que noté fue con el bloque de inicio local y la declaración de variable. El compilador local fue un poco más específico al nombrar las variables.
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
[1] int32 thing,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001)
Pero con esa pequeña diferencia, fue tan lejos, tan bueno. Tuve una salida IL equivalente entre el compilador DotNetFiddler y lo que estaba produciendo mi instancia VS local.
Entonces, reconstruí el proyecto dirigido a .NET 4, .NET 3.5 y, en buena medida, el modo de lanzamiento de .NET 3.5.
Y en los tres casos adicionales, la IL generada fue equivalente. La versión específica de .NET no tuvo ningún efecto sobre la IL que se generó en estas muestras.
Para resumir esta aventura: creo que podemos decir con confianza que al compilador no le importa dónde declara el tipo primitivo y que no hay ningún efecto sobre la memoria o el rendimiento con ninguno de los métodos de declaración. Y eso es cierto independientemente de usar un bucle for
o foreach
.
Pensé en ejecutar otro caso más que incorporaba un cierre dentro del foreach
bucle. Pero usted había preguntado acerca de los efectos de dónde se declaró una variable de tipo primitiva, así que pensé que estaba profundizando demasiado más allá de lo que le interesaba preguntar. La pregunta SO que mencioné anteriormente tiene una gran respuesta que proporciona una buena visión general sobre los efectos de cierre en las variables de iteración foreach.
1 Gracias a Andy por proporcionar el enlace original a la pregunta SO que aborda los cierres dentro de los foreach
bucles.
2 Vale la pena señalar que la especificación ECMA-335 aborda esto con la sección I.12.3.2.2 'Variables locales y argumentos'. Tuve que ver la IL resultante y luego leer la sección para que quede claro con respecto a lo que estaba sucediendo. Gracias a Ratchet Freak por señalar eso en el chat.