TL; DR - są to równoważne przykłady na warstwie IL.
DotNetFiddle sprawia, że odpowiedź jest ładna, ponieważ pozwala zobaczyć wynikową IL.
Użyłem nieco innej odmiany twojej konstrukcji pętli, aby przyspieszyć testowanie. Użyłem:
Wariant 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);
}
}
}
Wariant 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
W obu przypadkach skompilowane wyjście IL renderowało to samo.
.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
Aby odpowiedzieć na twoje pytanie: kompilator optymalizuje deklarację zmiennej i równoważy te dwie odmiany.
O ile mi wiadomo, kompilator .NET IL przenosi wszystkie deklaracje zmiennych na początek funkcji, ale nie mogłem znaleźć dobrego źródła, które wyraźnie stwierdziłoby, że 2 . W tym konkretnym przykładzie widać, że przesunęło to ich o następującą instrukcję:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
W tym przypadku stajemy się zbyt obsesyjni w dokonywaniu porównań ...
Przypadek A: czy wszystkie zmienne są przenoszone w górę?
Aby zagłębić się w to, przetestowałem następującą funkcję:
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);
}
}
Różnica polega na tym, że możemy zadeklarować OSOBĄ int i
lub string j
w oparciu o porównanie. Ponownie kompilator przenosi wszystkie zmienne lokalne na szczyt funkcji 2 za pomocą:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Zauważyłem, że warto zauważyć, że chociaż int i
nie zostanie zadeklarowany w tym przykładzie, kod do jego obsługi jest generowany.
Przypadek B: A może foreach
zamiast for
?
Wskazano, że foreach
ma inne zachowanie for
i że nie sprawdzałem tego, o co pytano. Wstawiłem więc te dwie sekcje kodu, aby porównać wynikową IL.
int
deklaracja poza pętlą:
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
deklaracja wewnątrz pętli:
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);
}
Powstała IL z foreach
pętlą rzeczywiście różniła się od IL wygenerowanej za pomocą for
pętli. W szczególności zmieniono blok inicjujący i sekcję pętli.
.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
foreach
Podejście bardziej zmienne generowane lokalnych i wymaga pewnych dodatkowych rozgałęzień. Zasadniczo za pierwszym razem przeskakuje na koniec pętli, aby uzyskać pierwszą iterację wyliczenia, a następnie przeskakuje z powrotem na prawie szczyt pętli, aby wykonać kod pętli. Następnie przechodzi przez pętlę, jak można się spodziewać.
Ale poza różnicami rozgałęziających spowodowane użyciem for
i foreach
konstrukcje, nie było bez różnicy w IL oparciu którym int i
zgłoszenie zostało złożone. Więc nadal jesteśmy przy dwóch podejściach równoważnych.
Przypadek C: Co z różnymi wersjami kompilatora?
W komentarzu, który pozostawiono 1 , był link do pytania SO dotyczącego ostrzeżenia o zmiennym dostępie z foreach i zamykaniem . Część, która naprawdę przykuła moją uwagę w tym pytaniu, polegała na tym, że mogły istnieć różnice w działaniu kompilatora .NET 4.5 w porównaniu z wcześniejszymi wersjami kompilatora.
I właśnie tam zawiodła mnie witryna DotNetFiddler - mieli tylko .NET 4.5 i wersję kompilatora Roslyn. Więc przywołałem lokalną instancję Visual Studio i zacząłem testować kod. Aby upewnić się, że porównuję te same rzeczy, porównałem lokalnie zbudowany kod w .NET 4.5 z kodem DotNetFiddler.
Jedyną różnicą, którą zauważyłem, był lokalny blok init i deklaracja zmiennej. Lokalny kompilator był nieco bardziej szczegółowy w nazywaniu zmiennych.
.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)
Ale z tą niewielką różnicą było to do tej pory tak dobre. Miałem równoważne wyjście IL między kompilatorem DotNetFiddler a tym, co produkowała moja lokalna instancja VS.
Więc następnie przebudowałem projekt ukierunkowany na .NET 4, .NET 3.5 i dla pewności tryb wydania .NET 3.5.
I we wszystkich tych trzech dodatkowych przypadkach wygenerowana IL była równoważna. Wybrana wersja .NET nie miała wpływu na IL wygenerowaną w tych próbkach.
Podsumowując tę przygodę: Myślę, że możemy śmiało powiedzieć, że kompilator nie dba o to, gdzie deklarujesz typ prymitywny i że nie ma to żadnego wpływu na pamięć ani wydajność żadnej z metod deklaracji. I to obowiązuje niezależnie od użycia pętli for
lub foreach
.
Zastanawiałem się nad uruchomieniem kolejnej sprawy, która zawiera zamknięcie wewnątrz foreach
pętli. Ale zapytałeś o skutki, gdzie zadeklarowano zmienną typu pierwotnego, więc pomyślałem, że sięgam zbyt daleko poza to, o co chciałeś zapytać. Pytanie SO, o którym wspomniałem wcześniej, ma świetną odpowiedź, która zapewnia dobry przegląd efektów zamknięcia na zmiennych iteracji foreach.
1 Dziękujemy Andy'emu za dostarczenie oryginalnego linku do pytania SO dotyczącego zamykania foreach
pętli.
2 Warto zauważyć, że specyfikacja ECMA-335 rozwiązuje ten problem w sekcji I.12.3.2.2 „Zmienne lokalne i argumenty”. Musiałem zobaczyć wynikową IL, a następnie przeczytać sekcję, aby było jasne, co się dzieje. Dzięki grzechotnikowi za wskazanie tego na czacie.