TL; DR : sono esempi equivalenti a livello di IL.
DotNetFiddle rende questo abbastanza rispondere in quanto consente di vedere l'IL risultante.
Ho usato una variante leggermente diversa del costrutto del tuo loop per rendere più veloci i miei test. Ero solito:
Variazione 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);
}
}
}
Variazione 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
In entrambi i casi, l'output IL compilato ha reso lo stesso.
.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
Quindi, per rispondere alla tua domanda: il compilatore ottimizza la dichiarazione della variabile e rende equivalenti le due varianti.
Per quanto ne so, il compilatore .NET IL sposta tutte le dichiarazioni variabili all'inizio della funzione ma non sono riuscito a trovare una buona fonte che affermasse chiaramente che 2 . In questo esempio particolare, vedi che li ha spostati verso l'alto con questa affermazione:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
In cui diventiamo un po 'troppo ossessivi nel fare confronti ....
Caso A, tutte le variabili vengono spostate verso l'alto?
Per approfondire ulteriormente questo aspetto, ho testato la seguente funzione:
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 differenza qui è che dichiariamo uno int i
o uno string j
basato sul confronto. Ancora una volta, il compilatore sposta tutte le variabili locali nella parte superiore della funzione 2 con:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Ho trovato interessante notare che anche se int i
non verrà dichiarato in questo esempio, il codice per supportarlo viene comunque generato.
Caso B: che dire foreach
invece di for
?
È stato sottolineato che foreach
ha un comportamento diverso rispetto a for
quello e che non stavo verificando la stessa cosa che mi era stata chiesta. Quindi ho inserito queste due sezioni di codice per confrontare l'IL risultante.
int
dichiarazione al di fuori del ciclo:
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
dichiarazione all'interno del ciclo:
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);
}
L'IL risultante con il foreach
loop era effettivamente diverso dall'IL generato utilizzando il for
loop. In particolare, il blocco init e la sezione loop sono cambiati.
.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
L' foreach
approccio ha generato più variabili locali e ha richiesto alcune ramificazioni aggiuntive. In sostanza, la prima volta passa alla fine del ciclo per ottenere la prima iterazione dell'enumerazione, quindi torna alla fine del ciclo per eseguire il codice del ciclo. Quindi continua a scorrere come ti aspetteresti.
Ma al di là delle differenze di ramificazione causate dall'uso dei costrutti for
e foreach
, non vi era alcuna differenza nell'IL in base al luogo in cui int i
era collocata la dichiarazione. Quindi siamo ancora ai due approcci che sono equivalenti.
Caso C: che dire delle diverse versioni del compilatore?
In un commento che era rimasto 1 , c'era un collegamento a una domanda SO riguardante un avviso sull'accesso variabile con foreach e l'uso della chiusura . La parte che ha davvero attirato la mia attenzione in quella domanda è che potrebbero esserci state differenze nel funzionamento del compilatore .NET 4.5 rispetto alle versioni precedenti del compilatore.
Ed è qui che il sito DotNetFiddler mi ha deluso: tutto ciò che avevano a disposizione era .NET 4.5 e una versione del compilatore Roslyn. Quindi ho richiamato un'istanza locale di Visual Studio e ho iniziato a testare il codice. Per essere sicuro che stavo confrontando le stesse cose, ho confrontato il codice creato localmente su .NET 4.5 con il codice DotNetFiddler.
L'unica differenza che ho notato era con il blocco init locale e la dichiarazione delle variabili. Il compilatore locale era un po 'più specifico nel nominare le variabili.
.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)
Ma con quella piccola differenza, era così lontano, così buono. Avevo un output IL equivalente tra il compilatore DotNetFiddler e ciò che la mia istanza VS locale stava producendo.
Quindi ho ricostruito il progetto destinato a .NET 4, .NET 3.5 e, per buona misura, alla modalità di rilascio di .NET 3.5.
E in tutti e tre questi casi aggiuntivi, l'IL generato era equivalente. La versione .NET mirata non ha avuto alcun effetto sull'IL che è stato generato in questi esempi.
Riassumendo questa avventura: penso che possiamo affermare con sicurezza che al compilatore non importa dove dichiari il tipo primitivo e che non c'è alcun effetto sulla memoria o sulle prestazioni con nessuno dei due metodi di dichiarazione. E questo vale indipendentemente dall'uso di a for
o foreach
loop.
Ho considerato di eseguire un altro caso che includeva una chiusura all'interno del foreach
ciclo. Ma avevi chiesto degli effetti di dove veniva dichiarata una variabile di tipo primitiva, quindi ho pensato che stavo scavando troppo oltre ciò di cui eri interessato. La domanda SO che ho menzionato in precedenza ha un'ottima risposta che fornisce una buona panoramica degli effetti di chiusura su ogni variabile di iterazione.
1 Grazie ad Andy per aver fornito il link originale alla domanda SO che affronta le chiusure all'interno dei foreach
loop.
2 Vale la pena notare che la specifica ECMA-335 risolve questo problema con la sezione I.12.3.2.2 "Variabili e argomenti locali". Ho dovuto vedere l'IL risultante e quindi leggere la sezione per essere chiaro riguardo a ciò che stava succedendo. Grazie a ratchet maniaco per averlo sottolineato in chat.