TL; DR-它们是IL层的等效示例。
DotNetFiddle使它很容易回答,因为它使您可以看到最终的IL。
我使用了稍微不同的循环构造形式,以加快测试速度。我用了:
变体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);
}
}
}
变体2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
在这两种情况下,编译后的IL输出都相同。
.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
因此,回答您的问题:编译器优化了变量的声明,并使两个变量等效。
据我了解,.NET IL编译器将所有变量声明移至函数的开头,但我找不到清楚说明2的良好来源。在这个特定的示例中,您看到它通过以下语句使它们向上移动:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
我们在进行比较时过于痴迷...
情况A,所有变量是否都向上移动?
为了进一步探讨这一点,我测试了以下功能:
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);
}
}
此处的区别是我们根据比较声明了an int i
或a string j
。再次,编译器使用以下命令将所有局部变量移至函数2的顶部:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
我发现有趣的是,即使int i
在此示例中未声明,也仍在生成支持该代码的代码。
案例B:foreach
而不是for
呢?
有人指出,这种foreach
行为for
与行为不同,而且我没有检查被问到的同一件事。因此,我放入了这两部分代码以比较最终的IL。
int
循环外的声明:
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
循环内的声明:
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);
}
带有foreach
循环的最终IL 确实不同于使用该for
循环生成的IL 。具体来说,init块和循环部分已更改。
.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
方法生成了更多的局部变量,并且需要一些额外的分支。本质上,它第一次跳转到循环的末尾以获得枚举的第一次迭代,然后又跳回到循环的几乎顶部以执行循环代码。然后,它会继续按您的期望循环。
但是,除了使用for
和foreach
构造引起的分支差异之外,基于声明的放置位置,IL中也没有差异int i
。因此,我们仍将两种方法等同。
情况C:不同的编译器版本如何?
在剩下的1条评论中,有一个SO问题的链接,该问题与有关使用foreach和使用闭包进行变量访问的警告有关。在这个问题中真正引起我注意的部分是.NET 4.5编译器的工作方式与早期版本的编译器可能存在差异。
那就是DotNetFiddler网站让我失望的地方-他们所能使用的只是.NET 4.5和Roslyn编译器的版本。因此,我调出了Visual Studio的本地实例并开始测试代码。为了确保我比较的是相同的东西,我将.NET 4.5的本地生成的代码与DotNetFiddler代码进行了比较。
我注意到的唯一区别是局部init块和变量声明。本地编译器在命名变量时更加具体。
.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)
但是,尽管有微小的差异,但到目前为止,一切都很好。我在DotNetFiddler编译器和本地VS实例生成的内容之间具有等效的IL输出。
因此,我然后重新构建了针对.NET 4,.NET 3.5以及很好的.NET 3.5 Release模式的项目。
在所有这三种其他情况下,生成的IL都是等效的。目标.NET版本对这些样本中生成的IL没有影响。
总结一下这一冒险过程: 我想我们可以自信地说,编译器不在乎声明原始类型的位置,并且使用这两种声明方法对内存或性能都没有影响。不管使用for
or foreach
循环,这都适用。
我考虑过运行另一种在foreach
循环内部合并闭包的情况。但是您曾经询问过在哪里声明原始类型变量的影响,所以我认为我的研究超出了您感兴趣的范围。我前面提到的SO问题有一个很好的答案,它很好地概述了对foreach迭代变量的关闭效果。
1 感谢Andy为foreach
循环中的闭包提供了SO问题的原始链接。
2 值得注意的是,ECMA-335规范在I.12.3.2.2节“局部变量和参数”中解决了此问题。我必须查看生成的IL,然后阅读本节以清楚了解发生了什么。感谢棘轮怪胎在聊天中指出这一点。