TL; DR - mereka adalah contoh yang setara di lapisan IL.
DotNetFiddle membuat ini cukup untuk menjawab karena memungkinkan Anda untuk melihat IL yang dihasilkan.
Saya menggunakan variasi yang sedikit berbeda dari konstruksi loop Anda untuk membuat pengujian saya lebih cepat. Saya menggunakan:
Variasi 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);
}
}
}
Variasi 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
Dalam kedua kasus, output IL yang dikompilasi menghasilkan hal yang sama.
.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
Jadi untuk menjawab pertanyaan Anda: kompiler mengoptimalkan deklarasi variabel, dan membuat dua variasi yang setara.
Untuk pemahaman saya, .NET IL compiler memindahkan semua deklarasi variabel ke awal fungsi tetapi saya tidak dapat menemukan sumber yang baik yang dengan jelas menyatakan bahwa 2 . Dalam contoh khusus ini, Anda melihat bahwa itu menggerakkan mereka dengan pernyataan ini:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
Di mana kita menjadi agak terlalu obsesif dalam membuat perbandingan ....
Kasus A, apakah semua variabel dipindahkan ke atas?
Untuk menggali sedikit lebih jauh, saya menguji fungsi berikut:
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);
}
}
Perbedaannya di sini adalah bahwa kami menyatakan salah int i
atau string j
berdasarkan perbandingan. Sekali lagi, kompilator memindahkan semua variabel lokal ke atas fungsi 2 dengan:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Saya merasa menarik untuk dicatat bahwa meskipun int i
tidak akan dideklarasikan dalam contoh ini, kode untuk mendukungnya masih dihasilkan.
Kasus B: Bagaimana kalau foreach
bukan for
?
Itu menunjukkan bahwa foreach
memiliki perilaku yang berbeda dari for
dan bahwa saya tidak memeriksa hal yang sama yang telah ditanyakan. Jadi saya memasukkan dua bagian kode ini untuk membandingkan IL yang dihasilkan.
int
deklarasi di luar 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
deklarasi di dalam 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);
}
IL yang dihasilkan dengan foreach
loop itu memang berbeda dari IL yang dihasilkan menggunakan for
loop. Secara khusus, blok init dan bagian loop berubah.
.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
The foreach
Pendekatan yang dihasilkan lebih variabel lokal dan diperlukan beberapa percabangan tambahan. Pada dasarnya, pada saat pertama di dalamnya melompat ke ujung loop untuk mendapatkan iterasi pertama enumerasi dan kemudian melompat kembali ke hampir bagian atas loop untuk menjalankan kode loop. Kemudian terus berlanjut seperti yang Anda harapkan.
Tetapi di luar perbedaan percabangan yang disebabkan oleh penggunaan for
dan foreach
konstruksi, tidak ada perbedaan dalam IL berdasarkan di mana int i
deklarasi ditempatkan. Jadi kita masih pada dua pendekatan yang setara.
Kasus C: Bagaimana dengan versi kompiler yang berbeda?
Dalam komentar yang tersisa 1 , ada tautan ke pertanyaan SO tentang peringatan tentang akses variabel dengan foreach dan menggunakan penutupan . Bagian yang benar-benar menarik perhatian saya dalam pertanyaan itu adalah bahwa mungkin ada perbedaan dalam cara kerja kompiler .NET 4.5 dibandingkan versi sebelumnya dari kompiler.
Dan di situlah situs DotNetFiddler mengecewakan saya - yang mereka miliki hanyalah .NET 4.5 dan versi dari kompiler Roslyn. Jadi saya membuka contoh lokal Visual Studio dan mulai menguji kode. Untuk memastikan saya membandingkan hal yang sama, saya membandingkan kode yang dibuat secara lokal di .NET 4.5 dengan kode DotNetFiddler.
Satu-satunya perbedaan yang saya perhatikan adalah dengan blok init lokal dan deklarasi variabel. Kompiler lokal sedikit lebih spesifik dalam penamaan variabel.
.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)
Tetapi dengan perbedaan kecil itu, sejauh ini, sangat bagus. Saya memiliki output IL yang setara antara kompiler DotNetFiddler dan apa yang diproduksi oleh instance VS lokal saya.
Jadi saya kemudian membangun kembali penargetan proyek .NET 4, .NET 3.5, dan untuk ukuran yang baik. NET 3.5 Rilis mode.
Dan dalam ketiga kasus tambahan tersebut, IL yang dihasilkan setara. Versi .NET yang ditargetkan tidak berpengaruh pada IL yang dihasilkan dalam sampel ini.
Untuk merangkum petualangan ini: Saya pikir kita dapat dengan yakin mengatakan bahwa kompiler tidak peduli di mana Anda mendeklarasikan tipe primitif dan bahwa tidak ada efek pada memori atau kinerja dengan metode deklarasi. Dan itu berlaku terlepas dari menggunakan a for
atau foreach
loop.
Saya mempertimbangkan menjalankan kasus lain yang memasukkan penutupan di dalam foreach
loop. Tetapi Anda telah bertanya tentang efek di mana variabel tipe primitif dideklarasikan, jadi saya pikir saya menggali terlalu jauh melampaui apa yang Anda minati. Pertanyaan SO yang saya sebutkan sebelumnya memiliki jawaban yang bagus yang memberikan tinjauan yang baik tentang efek penutupan pada variabel iteach masing-masing.
1 Terima kasih kepada Andy karena telah memberikan tautan asli ke pertanyaan SO yang membahas penutupan dalam foreach
loop.
2 Perlu dicatat bahwa spesifikasi ECMA-335 membahas hal ini dengan bagian I.12.3.2.2 'Variabel dan argumen lokal'. Saya harus melihat IL yang dihasilkan dan kemudian membaca bagian untuk menjadi jelas tentang apa yang sedang terjadi. Terima kasih kepada ratchet freak karena menunjukkan hal itu dalam obrolan.