是否有一个原因C#的重用的变量在Foreach?

在C#中使用lambda表达式或匿名方法时,我们必须警惕访问修改闭包陷阱。例如:

foreach (var s in strings){query = query.Where(i => i.Prop == s); // access to modified closure...}

由于修改了闭包,上述代码将导致查询上的所有Where子句都基于s的最终值。

正如这里所解释的,发生这种情况是因为上面foreach循环中声明的s变量在编译器中被翻译成这样:

string s;while (enumerator.MoveNext()){s = enumerator.Current;...}

而不是像这样:

while (enumerator.MoveNext()){string s;s = enumerator.Current;...}

正如这里所指出的,在循环之外声明变量没有性能优势,在正常情况下,我能想到的唯一原因是如果您计划在循环范围之外使用变量:

string s;while (enumerator.MoveNext()){s = enumerator.Current;...}var finalString = s;

但是,在foreach循环中定义的变量不能在循环之外使用:

foreach(string s in strings){}var finalString = s; // won't work: you're outside the scope.

因此,编译器声明变量的方式使其非常容易出现通常难以发现和调试的错误,同时不会产生任何可感知的好处。

有没有什么可以用foreach循环做的事情,如果它们是使用内部作用域变量编译的,你就不能做了,或者这只是一个任意的选择,在匿名方法和lambda表达式可用或常见之前做出的,从那时起就没有修改过?

116756 次浏览

Eric Lippert在他的博客文章关闭循环变量被认为是有害的及其续集中彻底涵盖了你所问的问题。

对我来说,最有说服力的论点是在每次迭代中拥有新变量将与for(;;)样式循环不一致。你希望在for (int i = 0; i < 10; i++)的每次迭代中都有一个新的int i吗?

这种行为最常见的问题是在迭代变量上创建闭包,它有一个简单的解决方法:

foreach (var s in strings){var s_for_closure = s;query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

关于这个问题的博客文章:C#中Foreach变量的闭包

受此困扰,我有一个习惯,即在最内层的作用域中包含局部定义的变量,我用它来转移到任何闭包。在你的例子中:

foreach (var s in strings)query = query.Where(i => i.Prop == s); // access to modified closure

我做:

foreach (var s in strings){string search = s;query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.}

一旦你有了这个习惯,你就可以在你实际上打算绑定到外部作用域的非常罕见情况下避免它。

编译器声明变量的方式使其非常容易出现通常难以发现和调试的错误,同时不会产生任何可感知的好处。

你的批评是完全有道理的。

我在这里详细讨论这个问题:

关闭被认为有害的循环变量

有没有什么你可以用这种方式来处理Foreach循环,而如果它们是使用内部作用域变量编译的,你就不能这样做了?或者这只是一个任意的选择,在匿名方法和lambda表达式可用或常见之前做出的,并且从那时起就没有修改过?

后者。C#1.0规范实际上没有说明循环变量是在循环主体内部还是外部,因为它没有明显的区别。当C#2.0引入闭包语义学时,选择将循环变量放在循环之外,与“for”循环一致。

我认为公平地说,所有人都对这个决定感到遗憾。这是C#中最糟糕的“陷阱”之一,我们将采取突破性的改变来解决它。在C#5中,foreach循环变量在逻辑上将是循环主体的里面,因此闭包每次都会获得一个新的副本。

for循环不会更改,并且更改不会“反向移植”到以前的C#版本。因此,您在使用此习惯用法时应该继续小心。

在C#5.0中,这个问题是固定的,您可以关闭循环变量并获得您期望的结果。

语言规范说:

8.8.4 Foreach声明

(…)

关于形式的前言

foreach (V v in x) embedded-statement

然后扩展到:

{E e = ((C)(x)).GetEnumerator();try {while (e.MoveNext()) {V v = (V)(T)e.Current;embedded-statement}}finally {… // Dispose e}}

(…)

v在这时候循环中的位置对于它的方式很重要中发生的任何匿名函数捕获嵌入式语句。例如:

int[] values = { 7, 9, 13 };Action f = null;foreach (var value in values){if (f == null) f = () => Console.WriteLine("First value: " + value);}f();

如果v被声明在这时候循环之外,它将被共享在所有迭代中,它在for循环之后的值将是最终值13,这是调用f将打印的值。相反,因为每个迭代都有自己的变量v,即在第一次迭代中被f捕获将继续保存该值7,这是将要打印的内容。(注意:早期版本的C#声明v在这时候循环之外。