C#';s重用foreach中的变量

本文关键字:foreach 变量 重用 | 更新日期: 2023-09-27 18:20:29

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

C#';s重用foreach中的变量

编译器声明变量的方式使其极易出现错误,而这种错误通常很难找到和调试,同时不会产生明显的好处。

你的批评是完全合理的。

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

闭环变量被认为是有害的

如果foreach循环是用内部作用域变量编译的,那么您是否可以用这种方式处理foreach循环?还是这只是在匿名方法和lambda表达式可用或通用之前做出的任意选择,从那时起就没有修改过?

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

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

for循环将不会更改,并且更改不会"后移植"到以前版本的C#。因此,在使用这个成语时,你应该继续小心。

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.
}        

一旦你有了这个习惯,你就可以在非常的情况下避免它,因为你实际上打算绑定到外部作用域。老实说,我想我从来没有这样做过。

在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调用将打印的值。相反,因为每个迭代都有自己的变量vf在第一次迭代中捕获的值将继续保持7,这是将要打印的内容。(注意:C的早期版本#在while循环之外声明了v