TL; DR - são exemplos equivalentes na camada IL.
O DotNetFiddle torna isso bonito de responder, pois permite ver a IL resultante.
Usei uma variação ligeiramente diferente da sua construção de loop para tornar meus testes mais rápidos. Eu usei:
Variação 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);
}
}
}
Variação 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
Nos dois casos, a saída IL compilada foi renderizada da mesma forma.
.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
Então, para responder à sua pergunta: o compilador otimiza a declaração da variável e torna as duas variações equivalentes.
Para meu entendimento, o compilador .NET IL move todas as declarações de variáveis para o início da função, mas não consegui encontrar uma boa fonte que indicasse claramente 2 . Neste exemplo em particular, você vê que os moveu com esta declaração:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
Onde ficamos um pouco obsessivos em fazer comparações ...
Caso A, todas as variáveis são movidas para cima?
Para aprofundar um pouco mais, testei a seguinte função:
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);
}
}
A diferença aqui é que declaramos um int i
ou um string j
com base na comparação. Novamente, o compilador move todas as variáveis locais para o topo da função 2 com:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Achei interessante notar que, embora int i
não seja declarado neste exemplo, o código para suportá-lo ainda é gerado.
Caso B: E em foreach
vez de for
?
Foi apontado que ele foreach
tem um comportamento diferente for
e que eu não estava verificando a mesma coisa que havia sido perguntada. Então, eu coloquei nessas duas seções de código para comparar a IL resultante.
int
declaração fora do loop:
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
declaração dentro do loop:
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);
}
A IL resultante com o foreach
loop era de fato diferente da IL gerada usando o for
loop. Especificamente, o bloco init e a seção do loop foram alterados.
.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
A foreach
abordagem gerou mais variáveis locais e exigiu algumas ramificações adicionais. Essencialmente, na primeira vez, ele pula para o final do loop para obter a primeira iteração da enumeração e depois volta para quase a parte superior do loop para executar o código do loop. Em seguida, continua a percorrer conforme o esperado.
Porém, além das diferenças de ramificação causadas pelo uso das construções for
e foreach
, não houve diferença na IL com base no local em que a int i
declaração foi colocada. Portanto, ainda estamos nas duas abordagens sendo equivalentes.
Caso C: E as diferentes versões do compilador?
Em um comentário deixado 1 , havia um link para uma pergunta de SO referente a um aviso sobre acesso variável ao foreach e uso de fechamento . A parte que realmente chamou minha atenção nessa pergunta foi que pode ter havido diferenças em como o compilador .NET 4.5 funcionava em relação às versões anteriores do compilador.
E foi aí que o site DotNetFiddler me decepcionou - tudo o que eles tinham disponível era o .NET 4.5 e uma versão do compilador Roslyn. Então, criei uma instância local do Visual Studio e comecei a testar o código. Para garantir que eu comparasse as mesmas coisas, comparei o código criado localmente no .NET 4.5 com o código DotNetFiddler.
A única diferença que notei foi com o bloco init local e a declaração da variável. O compilador local foi um pouco mais específico ao nomear as variáveis.
.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)
Mas com essa pequena diferença, foi tão longe, tão bom. Eu tinha saída IL equivalente entre o compilador DotNetFiddler e o que minha instância local do VS estava produzindo.
Então, reconstruí o projeto direcionado ao .NET 4, .NET 3.5 e, em boa medida, ao modo .NET 3.5 Release.
E nos três casos adicionais, a IL gerada era equivalente. A versão do .NET direcionada não teve efeito na IL gerada nessas amostras.
Para resumir esta aventura: Acho que podemos dizer com segurança que o compilador não se importa com o local onde você declara o tipo primitivo e que não há efeito na memória ou no desempenho com qualquer método de declaração. E isso é válido independentemente do uso de um for
ou foreach
loop.
Eu considerei executar outro caso que incorporava um fechamento dentro do foreach
loop. Mas você perguntou sobre os efeitos de onde uma variável do tipo primitiva foi declarada, então imaginei que estava indo muito além do que você estava interessado em perguntar. A pergunta SO mencionada anteriormente tem uma ótima resposta que fornece uma boa visão geral sobre os efeitos de fechamento nas variáveis de iteração foreach.
1 Obrigado a Andy por fornecer o link original para a pergunta SO, abordando fechamentos em foreach
loops.
2 Vale ressaltar que a especificação do ECMA-335 trata disso na seção I.12.3.2.2 'Variáveis e argumentos locais'. Eu tive que ver a IL resultante e depois ler a seção para ficar claro sobre o que estava acontecendo. Obrigado à catraca por apontar isso no chat.