在 C# 中使用 lambda 表达式或匿名方法时,我们必须警惕访问修改后的闭包陷阱。例如:
foreach (var s in strings)
{
query = query.Where(i => i.Prop == s); // access to modified closure
...
}
由于修改了闭包,上述代码将导致查询中的所有 Where
子句都基于 s
的最终值。
正如 here 所解释的,发生这种情况是因为在上面的 foreach
循环中声明的 s
变量在编译器中被翻译成这样:
string s;
while (enumerator.MoveNext())
{
s = enumerator.Current;
...
}
而不是这样:
while (enumerator.MoveNext())
{
string s;
s = enumerator.Current;
...
}
正如 here 所指出的,在循环外声明变量没有性能优势,在正常情况下,我能想到这样做的唯一原因是,如果您打算在循环范围之外使用变量:
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 表达式可用或常见之前做出的任意选择,从那以后哪些没有被修改过?
String s; foreach (s in strings) { ... }
有什么问题?
foreach
,而是在谈论 lamda 表达式,导致类似的代码,如 OP 所示......
编译器声明变量的方式使其极易出现通常难以查找和调试的错误,同时不会产生明显的好处。
你的批评完全有道理。
我在这里详细讨论这个问题:
Closing over the loop variable considered harmful
如果使用内部范围的变量编译它们,您是否可以通过这种方式对 foreach 循环执行某些操作?或者这只是在匿名方法和 lambda 表达式可用或常见之前做出的任意选择,并且从那时起就没有修改过?
后者。 C# 1.0 规范实际上并没有说明循环变量是在循环体内部还是外部,因为它没有明显的区别。在 C# 2.0 中引入闭包语义时,选择将循环变量放在循环之外,与“for”循环一致。
我认为公平地说,所有人都对这个决定感到遗憾。这是 C# 中最糟糕的“陷阱”之一,我们将采取重大更改来修复它。在 C# 5 中,foreach 循环变量在逻辑上位于循环体中,因此闭包每次都会获得一个新副本。
for
循环不会更改,并且更改不会“向后移植”到以前的 C# 版本。因此,在使用此成语时应继续小心。
Eric Lippert 在他的博文 Closing over the loop variable considered harmful 及其续篇中全面介绍了您的问题。
对我来说,最有说服力的论点是每次迭代都有新变量与 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
我关于此问题的博文:Closure over foreach variable in C#。
受此困扰,我习惯于将本地定义的变量包含在我用来转移到任何闭包的最内层范围内。在您的示例中:
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 (V v in x) 嵌入式语句的 foreach 语句然后扩展为: { E e = ((C)(x)).GetEnumerator();尝试 { 而 (e.MoveNext()) { V v = (V)(T)e.Current; Embedded-statement } } finally { ... // 处理 e } } (...) v 在 while 循环中的位置对于嵌入语句中出现的任何匿名函数如何捕获它很重要。例如: int[] values = { 7, 9, 13 };动作 f = null; foreach (var value in values) { if (f == null) f = () => Console.WriteLine("第一个值:" + value); } F();如果 v 在 while 循环之外声明,它将在所有迭代之间共享,并且它在 for 循环之后的值将是最终值 13,这就是 f 调用将打印的值。相反,因为每次迭代都有自己的变量 v,所以在第一次迭代中被 f 捕获的变量将继续保持值 7,这就是将要打印的值。 (注意:早期版本的 C# 在 while 循环之外声明了 v。)
不定期副业成功案例分享
foreach
是“安全的”,但for
不是。