C#:是否有理由在foreach中重新使用变量?

Is there a reason for C#'s reuse of the variable in a foreach?

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

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

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

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

1
2
3
4
5
6
string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

而不是这样:

1
2
3
4
5
6
while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

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

1
2
3
4
5
6
7
string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

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

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

因此,编译器声明变量的方式使得它极易出错,这通常很难找到和调试,但不会产生可感知的好处。

如果使用内部作用域变量编译foreach循环,您是否可以用这种方式处理它们,或者这只是在匿名方法和lambda表达式可用或常见之前所做的一种任意选择,并且从那时起就没有对其进行过修改?


The compiler declares the variable in a way that makes it highly prone to an error that is often difficult to find and debug, while producing no perceivable benefits.

你的批评是完全合理的。

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

闭环变量被认为是有害的

Is there something you can do with foreach loops this way that you couldn't if they were compiled with an inner-scoped variable? or is this just an arbitrary choice that was made before anonymous methods and lambda expressions were available or common, and which hasn't been revised since then?

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

我认为公平地说,所有人都对这个决定感到遗憾。这是C中最糟糕的"gotchas"之一,我们将用破坏性的变化来修复它。在C 5中,foreach循环变量在逻辑上位于循环体内部,因此每次闭包都会得到一个新的副本。

for循环将不会被更改,更改也不会"返回"到以前的C版本。因此,在使用这个成语时应该继续小心。


埃里克·利珀特在他的博客文章《封闭循环变量》(closing over-the-loop variable)中对你的要求进行了全面的阐述,该变量被认为是有害的,并且是其续集。

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

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

1
2
3
4
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变量的闭包。


在被这个咬了之后,我有一个习惯,将本地定义的变量包含在最内部的作用域中,我使用它来传输到任何一个闭包。在您的示例中:

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

我愿意:

1
2
3
4
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 The foreach statement

(...)

A foreach statement of the form

1
foreach (V v in x) embedded-statement

is then expanded to:

1
2
3
4
5
6
7
8
9
10
11
12
{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
      … // Dispose e
  }
}

(...)

The placement of v inside the while loop is important for how it is
captured by any anonymous function occurring in the
embedded-statement. For example:

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

If v was declared outside of the while loop, it would be shared
among all iterations, and its value after the for loop would be the
final value, 13, which is what the invocation of f would print.
Instead, because each iteration has its own variable v, the one
captured by f in the first iteration will continue to hold the value
7, which is what will be printed. (Note: earlier versions of C#
declared v outside of the while loop.)


在我看来,这是个奇怪的问题。知道编译器是如何工作的很好,但那只是"很好知道"。

如果你写的代码依赖于编译器的算法,那是不好的做法,最好重写代码来排除这种依赖。

这是面试的好问题。但在现实生活中,我并没有遇到任何在面试中解决的问题。

for each的90%用于处理集合的每个元素(不用于选择或计算某些值)。有时需要在循环中计算一些值,但创建大循环并不是一个好的实践。

最好使用LINQ表达式来计算值。因为当你在循环中计算很多东西时,2-3个月后,当你(或其他任何人)将阅读这个代码时,人们将无法理解这是什么以及它应该如何工作。