在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#5.0中,这个问题得到了解决,您可以关闭循环变量并获得预期的结果。

语言规范说:

8.8.4 foreach语句(...)表单的foreach语句foreach(V V in x)嵌入语句然后扩展为:{E E=((C)(x)).GetEnumerator();尝试{同时(e.MoveNext()){V V=(V)(T)e.电流;嵌入式语句}}最后{…//处理}}(...)v在while循环中的位置对于它的状态很重要中发生的任何匿名函数捕获嵌入式语句。例如:int[]值={7,9,13};动作f=空;foreach(值中的var值){如果(f==null)f=()=>Console.WriteLine(“第一个值:”+值);}f();如果在while循环之外声明了v,那么它将被共享在所有迭代中,for循环后的值将是最终值13,这是f调用将打印的值。相反,因为每个迭代都有自己的变量v在第一次迭代中被f捕获的值将继续保持7,这是将要打印的内容。(注:C的早期版本#在while循环之外声明了v。)

其他回答

在C#5.0中,这个问题得到了解决,您可以关闭循环变量并获得预期的结果。

语言规范说:

8.8.4 foreach语句(...)表单的foreach语句foreach(V V in x)嵌入语句然后扩展为:{E E=((C)(x)).GetEnumerator();尝试{同时(e.MoveNext()){V V=(V)(T)e.电流;嵌入式语句}}最后{…//处理}}(...)v在while循环中的位置对于它的状态很重要中发生的任何匿名函数捕获嵌入式语句。例如:int[]值={7,9,13};动作f=空;foreach(值中的var值){如果(f==null)f=()=>Console.WriteLine(“第一个值:”+值);}f();如果在while循环之外声明了v,那么它将被共享在所有迭代中,for循环后的值将是最终值13,这是f调用将打印的值。相反,因为每个迭代都有自己的变量v在第一次迭代中被f捕获的值将继续保持7,这是将要打印的内容。(注:C的早期版本#在while循环之外声明了v。)

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#版本。因此,在使用这个习语时,您应该继续小心。