日期:2014-05-18  浏览次数:20817 次

关于foreach语句在C#4.5中的改进
看这样一段代码,你觉得会输出什么呢?

C# code
int[] data = new int[] { 1, 2, 3, 4, 5 };
List<Func<int>> actions = new List<Func<int>>();
foreach (int x in data)
{
    actions.Add(() => x);
}
foreach (var foo in actions)
{
    Console.WriteLine(foo());
}


如果你使用的是C# 4.0,运行结果是55555。

不要感到吃惊,因为在 C# 4.0 中,foreach的实现是这样的:

C# code
int[] data = new int[] { 1, 2, 3, 4, 5 };
List<Func<int>> actions = new List<Func<int>>();
IEnumerator e = data.GetEnumerator();

int x = 0;
while (e.MoveNext())
{
    x = (int)e.Current;
    actions.Add(() => x);
}

foreach (var foo in actions)
{
    Console.WriteLine(foo());
}


注意迭代变量x是在循环块外部被定义的。

这里涉及到一个很重要的概念,闭包,在Lambda表达式中,我们使用了外层的自由变量x,注意,在调用lambda表达式的时候,x会被求值,而这个定义在外部的x变量在循环终了等于5,这是为什么都是输出5的原因。

但是对于大多数程序员,他们希望的输出是12345,我们把上面的代码修改下:
C# code
int[] data = new int[] { 1, 2, 3, 4, 5 };
List<Func<int>> actions = new List<Func<int>>();
IEnumerator e = data.GetEnumerator();

while (e.MoveNext())
{
    int x = 0;
    x = (int)e.Current;
    actions.Add(() => x);
}

foreach (var foo in actions)
{
    Console.WriteLine(foo());
}


这一次,我们将x定义到块的内部。因此每当循环执行一次,都会产生一个局部变量x,闭包就会对每一个迭代单独求值,所以输出就是我们期望的12345了。

因为这个问题,在C# 4.0时代,我们必须非常小心foreach对闭包的影响,在C# 4.5(VS11 Beta)中,编译器终于做出了改变。

回到开头的代码,在VS11 Beta中会产生12345的输出了。

最后说一下,如果你希望编写出C# 4.0和C# 4.5编译完全一致的代码,你可以这么写:
C# code
int[] data = new int[] { 1, 2, 3, 4, 5 };
List<Func<int>> actions = new List<Func<int>>();
foreach (int x in data)
{
    int x1 = x;
    actions.Add(() => x1);
}
foreach (var foo in actions)
{
    Console.WriteLine(foo());
}


------解决方案--------------------
我猜是12345
------解决方案--------------------
研究的很细致!
------解决方案--------------------
我了解了,
------解决方案--------------------
还没注意到4.5有这改进,很好。

闭包是个坑爹的陷阱
------解决方案--------------------



明天去公司用vs11试试看。。
------解决方案--------------------
探讨
明天去公司用vs11试试看。。

------解决方案--------------------
Closure is very useful in JavsScript, but for C#... I've never met any situation where I use it.
------解决方案--------------------
这样会养成不良的编程习惯

从概念上理解(() => x);将编译成一个方法,而x的内存地址被固定了。
foreach (var foo in actions)里面应该始终会被调用成一个返回值。

在其他语言里面,都是这样的,所以不应该这么写。
------解决方案--------------------
认真撸过
------解决方案--------------------
vs11里目标选4或者4.5,运行结果都是12345
------解决方案--------------------
js特有的吧?int型不是引用,理论上应该是12345才对,没试过,以后试试
------解决方案--------------------
探讨
这充分说明foreach是C#语法糖,和CLR完全无关。