TL; DR - das sind äquivalente Beispiele auf der IL-Ebene.
DotNetFiddle macht dies hübsch zu beantworten, da Sie die resultierende IL sehen können.
Ich habe eine etwas andere Variante Ihres Schleifenkonstrukts verwendet, um meine Tests zu beschleunigen. Ich benutzte:
Variante 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);
}
}
}
Variante 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
In beiden Fällen wurde die kompilierte IL-Ausgabe gleich wiedergegeben.
.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
Um Ihre Frage zu beantworten: Der Compiler optimiert die Deklaration der Variablen und macht die beiden Variationen gleich.
Nach meinem Verständnis verschiebt der .NET IL-Compiler alle Variablendeklarationen an den Anfang der Funktion, aber ich konnte keine gute Quelle finden, aus der eindeutig hervorgeht, dass 2 . In diesem Beispiel sehen Sie, dass sie mit dieser Anweisung nach oben verschoben wurden:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
Wobei wir ein bisschen zu besessen sind, wenn wir Vergleiche anstellen ...
Fall A: Werden alle Variablen nach oben verschoben?
Um dies etwas näher zu untersuchen, habe ich die folgende Funktion getestet:
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);
}
}
Der Unterschied besteht darin, dass wir basierend auf dem Vergleich entweder ein int i
oder ein deklarieren string j
. Wieder verschiebt der Compiler alle lokalen Variablen an den Anfang der Funktion 2 mit:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Ich fand es interessant festzustellen, dass int i
der Code, der ihn unterstützt, immer noch generiert wird , obwohl er in diesem Beispiel nicht deklariert wird.
Fall B: Was ist mit foreach
statt for
?
Es wurde darauf hingewiesen, dass sich foreach
das anders verhält als for
und dass ich nicht das Gleiche überprüfe, nach dem gefragt wurde. Also habe ich diese beiden Codeabschnitte eingefügt, um die resultierende IL zu vergleichen.
int
Deklaration außerhalb der Schleife:
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
Deklaration innerhalb der Schleife:
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);
}
Die resultierende IL mit der foreach
Schleife unterschied sich tatsächlich von der IL, die unter Verwendung der for
Schleife erzeugt wurde. Insbesondere wurden der Init-Block und der Schleifenabschnitt geändert.
.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
Der foreach
Ansatz erzeugte mehr lokale Variablen und erforderte einige zusätzliche Verzweigungen. Im Wesentlichen springt es beim ersten Mal zum Ende der Schleife, um die erste Iteration der Aufzählung zu erhalten, und springt dann fast zum Anfang der Schleife zurück, um den Schleifencode auszuführen. Es wird dann wie erwartet weiter durchlaufen.
Abgesehen von den Verzweigungsunterschieden, die durch die Verwendung der Konstrukte for
und verursacht wurden foreach
, gab es keinen Unterschied in der IL, basierend darauf, wo die int i
Deklaration platziert wurde. Wir sind also immer noch der Meinung, dass beide Ansätze gleichwertig sind.
Fall C: Was ist mit verschiedenen Compilerversionen?
In einem Kommentar, der 1 hinterlassen wurde , gab es einen Link zu einer SO-Frage bezüglich einer Warnung über den variablen Zugriff mit foreach und die Verwendung von Closure . Der Teil, der mir bei dieser Frage wirklich aufgefallen ist, war, dass es möglicherweise Unterschiede in der Funktionsweise des .NET 4.5-Compilers gegenüber früheren Versionen des Compilers gab.
Und hier hat mich die DotNetFiddler-Site im Stich gelassen - alles, was sie zur Verfügung hatten, war .NET 4.5 und eine Version des Roslyn-Compilers. Also habe ich eine lokale Instanz von Visual Studio aufgerufen und angefangen, den Code zu testen. Um sicherzustellen, dass ich dieselben Dinge verglichen habe, habe ich lokal erstellten Code in .NET 4.5 mit dem DotNetFiddler-Code verglichen.
Der einzige Unterschied, den ich feststellte, war der lokale Init-Block und die Variablendeklaration. Der lokale Compiler war bei der Benennung der Variablen etwas spezifischer.
.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)
Aber mit diesem kleinen Unterschied war es so weit, so gut. Ich hatte eine äquivalente IL-Ausgabe zwischen dem DotNetFiddler-Compiler und dem, was meine lokale VS-Instanz produzierte.
Also habe ich das Projekt für .NET 4, .NET 3.5 und zum guten Teil für den .NET 3.5 Release-Modus neu erstellt.
In allen drei zusätzlichen Fällen war die generierte IL gleichwertig. Die anvisierte .NET-Version hatte keine Auswirkung auf die IL, die in diesen Beispielen generiert wurde.
Um dieses Abenteuer zusammenzufassen: Ich denke, wir können mit Sicherheit sagen, dass es dem Compiler egal ist, wo Sie den primitiven Typ deklarieren, und dass es bei beiden Deklarationsmethoden keine Auswirkungen auf den Speicher oder die Leistung gibt. Und das gilt unabhängig von der Verwendung einer for
oder foreach
-Schleife.
Ich überlegte, noch einen weiteren Fall auszuführen, der einen Verschluss innerhalb der foreach
Schleife enthielt . Aber Sie hatten nach den Auswirkungen der Deklaration einer primitiven Typvariablen gefragt, und ich dachte, ich würde zu weit über das hinausgehen, worüber Sie interessiert waren. Die zuvor erwähnte SO-Frage hat eine großartige Antwort , die einen guten Überblick über die Schließungseffekte für jede Iterationsvariable bietet.
1 Vielen Dank an Andy für die Bereitstellung des ursprünglichen Links zur SO-Frage zu Schließungen innerhalb von foreach
Schleifen.
2 Es ist erwähnenswert, dass die ECMA-335-Spezifikation dies mit Abschnitt I.12.3.2.2 'Lokale Variablen und Argumente' behandelt. Ich musste die resultierende IL sehen und dann den Abschnitt lesen, damit klar wurde, was los war. Vielen Dank an den Ratschenfreak, der im Chat darauf hingewiesen hat.