TL; DR - chúng là những ví dụ tương đương ở lớp IL.
DotNetFiddle làm cho câu trả lời này khá hay vì nó cho phép bạn xem IL kết quả.
Tôi đã sử dụng một biến thể hơi khác nhau của cấu trúc vòng lặp của bạn để làm cho thử nghiệm của tôi nhanh hơn. Tôi đã sử dụng:
Biến thể 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);
}
}
}
Biến thể 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
Trong cả hai trường hợp, đầu ra IL được biên dịch đều giống nhau.
.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
Vì vậy, để trả lời câu hỏi của bạn: trình biên dịch tối ưu hóa khai báo biến và hiển thị hai biến thể tương đương.
Theo hiểu biết của tôi, trình biên dịch .NET IL di chuyển tất cả các khai báo biến sang đầu hàm nhưng tôi không thể tìm thấy một nguồn tốt có ghi rõ ràng rằng 2 . Trong ví dụ cụ thể này, bạn thấy rằng nó đã di chuyển chúng lên với câu lệnh này:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
Trong đó chúng ta có một chút quá ám ảnh trong việc so sánh ....
Trường hợp A, tất cả các biến được di chuyển lên?
Để tìm hiểu sâu hơn một chút, tôi đã thử nghiệm chức năng sau:
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);
}
}
Sự khác biệt ở đây là chúng tôi tuyên bố một int i
hoặc string j
dựa trên so sánh. Một lần nữa, trình biên dịch di chuyển tất cả các biến cục bộ lên đầu hàm 2 với:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Tôi thấy thật thú vị khi lưu ý rằng mặc dù int i
sẽ không được khai báo trong ví dụ này, mã để hỗ trợ nó vẫn được tạo.
Trường hợp B: Thế còn foreach
thay vì for
?
Nó đã chỉ ra rằng foreach
có hành vi khác với for
và tôi đã không kiểm tra điều tương tự đã được hỏi về. Vì vậy, tôi đưa vào hai phần mã này để so sánh IL kết quả.
int
khai báo bên ngoài vòng lặp:
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
khai báo bên trong vòng lặp:
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);
}
Kết quả IL với foreach
vòng lặp thực sự khác với IL được tạo bằng for
vòng lặp. Cụ thể, khối init và phần vòng lặp đã thay đổi.
.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
Cách foreach
tiếp cận tạo ra nhiều biến cục bộ hơn và yêu cầu một số nhánh bổ sung. Về cơ bản, trong lần đầu tiên, nó nhảy đến cuối vòng lặp để có lần lặp đầu tiên của phép liệt kê và sau đó nhảy trở lại gần như đỉnh của vòng lặp để thực thi mã vòng lặp. Sau đó nó tiếp tục lặp lại như bạn mong đợi.
Nhưng ngoài sự khác biệt phân nhánh gây ra bởi việc sử dụng for
và foreach
cấu trúc, không có sự khác biệt nào trong IL dựa trên vị trí int i
khai báo được đặt. Vì vậy, chúng tôi vẫn ở hai cách tiếp cận tương đương nhau.
Trường hợp C: Còn các phiên bản trình biên dịch khác nhau thì sao?
Trong một bình luận còn lại 1 , có một liên kết đến một câu hỏi SO liên quan đến một cảnh báo về quyền truy cập biến đổi với foreach và sử dụng bao đóng . Phần thực sự khiến tôi chú ý trong câu hỏi đó là có thể có sự khác biệt về cách trình biên dịch .NET 4.5 hoạt động so với các phiên bản trước đó của trình biên dịch.
Và đó là nơi mà trang web DotNetFiddler làm tôi thất vọng - tất cả những gì họ có là .NET 4.5 và một phiên bản của trình biên dịch Roslyn. Vì vậy, tôi đã đưa ra một phiên bản địa phương của Visual Studio và bắt đầu thử nghiệm mã. Để chắc chắn rằng tôi đang so sánh những điều tương tự, tôi đã so sánh mã được xây dựng cục bộ tại .NET 4.5 với mã DotNetFiddler.
Sự khác biệt duy nhất mà tôi lưu ý là với khối init cục bộ và khai báo biến. Trình biên dịch cục bộ cụ thể hơn một chút trong việc đặt tên các biến.
.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)
Nhưng với sự khác biệt nhỏ đó, nó đã rất tốt. Tôi đã có đầu ra IL tương đương giữa trình biên dịch DotNetFiddler và phiên bản VS cục bộ của tôi đang tạo ra.
Vì vậy, sau đó tôi đã xây dựng lại dự án nhắm mục tiêu .NET 4, .NET 3.5 và để có chế độ phát hành .NET 3.5 tốt.
Và trong cả ba trường hợp bổ sung đó, IL được tạo ra đều tương đương. Phiên bản .NET được nhắm mục tiêu không có tác dụng đối với IL được tạo trong các mẫu này.
Để tóm tắt cuộc phiêu lưu này: Tôi nghĩ rằng chúng ta có thể tự tin nói rằng trình biên dịch không quan tâm đến việc bạn khai báo kiểu nguyên thủy ở đâu và rằng không có ảnh hưởng đến bộ nhớ hoặc hiệu suất với phương thức khai báo. Và điều đó đúng cho dù sử dụng vòng lặp for
hay foreach
.
Tôi đã cân nhắc việc chạy một trường hợp khác có kết hợp đóng bên trong foreach
vòng lặp. Nhưng bạn đã hỏi về tác động của nơi một biến loại nguyên thủy được khai báo, vì vậy tôi đoán rằng tôi đang đi quá xa so với những gì bạn quan tâm khi hỏi về. Câu hỏi SO tôi đã đề cập trước đó có một câu trả lời tuyệt vời cung cấp một cái nhìn tổng quan tốt về các hiệu ứng đóng đối với các biến lặp foreach.
1 Cảm ơn Andy vì đã cung cấp liên kết ban đầu đến các câu hỏi SO giải quyết các lần đóng trong foreach
vòng lặp.
2 Điều đáng chú ý là thông số kỹ thuật ECMA-335 giải quyết vấn đề này với phần I.12.3.2.2 'Các biến và đối số cục bộ'. Tôi đã phải xem IL kết quả và sau đó đọc phần cho nó rõ ràng về những gì đang xảy ra. Cảm ơn ratchet freak đã chỉ ra rằng trong trò chuyện.