C#在 foreach 中重用变量是否有原因?

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

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

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

正如解释在这里 ,出现这种情况是因为s变量中声明foreach循环以上的编译器编译如下:

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 表达式可用或通用之前做出的任意选择,以及从那以后没有修改过?

答案

编译器以使变量极易出现通常难以查找和调试的错误的方式声明变量,而不会产生明显的好处。

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

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

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

使用 foreach 循环,是否可以通过内部作用域变量进行编译而无法做到?还是这只是在匿名方法和 lambda 表达式可用或通用之前做出的任意选择,并且此后没有进行过修改?

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

我认为可以说所有人都对该决定表示遗憾。这是 C#中最糟糕的 “陷阱” 之一, 我们将进行重大更改来修复它。在 C#5 中,foreach 循环变量在逻辑上将位于循环体内,因此闭包每次都会获得新的副本。

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

你所问的是彻底埃里克利珀在他的博客盖关闭了循环变量认为是有害的和它的续集。

对我来说,最有说服力的论据是,每次迭代中都有新变量与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.

一旦有了这种习惯,在实际打算绑定到外部范围的少数情况下,就可以避免这种习惯。老实说,我认为我从未这样做过。

在 C#5.0 中,此问题已修复,您可以关闭循环变量并获得所需的结果。

语言规范说:

8.8.4 foreach 语句

(...)

形式的 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在 while 循环中的位置对于嵌入式语句中出现的任何匿名函数如何捕获它很重要。例如:

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

如果v在 while 循环之外声明,它将在所有迭代之间共享,并且 for 循环之后的值将是最终值13 ,这是对f的调用的输出。相反,因为每个迭代都有其自己的变量v ,所以在第一次迭代中被f捕获的变量将继续保持值7 ,该值将被打印。 ( 注意:C#的早期版本在 while 循环之外声明了v