提问



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


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


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


正如这里所解释的那样,这是因为上面foreach循环中声明的s变量在编译器中被翻译成这样:[30]


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

最佳参考



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



你的批评是完全合理的。


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


关闭循环变量被视为有害[32]



  有没有东西可以用foreach循环这样做,你不能用它们编译一个内部变量?或者这只是在匿名方法和lambda表达式可用或普通之前做出的任意选择,以及从那以后还没有被修改过?



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


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


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

其它参考1


Eric Lippert在他的博客文章中完全涵盖了你所要求的内容。关闭循环变量被认为是有害的及其续集。 [33]


对我来说,最有说服力的论点是在每次迭代中使用新变量将与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变量。[34]

其它参考2


受到这种困扰,我习惯在最里面的范围中包含本地定义的变量,我用它来转移到任何闭包。在你的例子中:


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.


一旦你有这种习惯,你可以在非常罕见的情况下避免它,你实际上打算绑定到外部范围。说实话,我不认为我曾经这样做过。

其它参考3


在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();

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


其它参考4


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


如果编写依赖于编译器算法的代码,这是不好的做法。重写代码以排除这种依赖性会更好。


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


90%的foreach用于处理每个集合元素(不用于选择或计算某些值)。有时您需要在循环内部计算一些值,但创建BIG循环并不是一个好习惯。


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