CSharp - C#重用foreach中的变量有理由吗?

  显示原文与译文双语对照的内容

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


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

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

就像刚才这里 ,发生这种情况是因为 s 变量上面 foreach 循环中声明是翻译的跟这编译器:


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 循环这种方式你完全不能与一个inner-scoped变量,或者是这只是一个主观的选择,如果他们被编译匿名方法和lamda表达式是可用或者之前进行一常见,并且它没有被修改后的自那以后?

时间:

编译器以一种方法声明变量,使它的极易发现和调试错误,而不产生可以感知的好处。

你的批评完全是合理的。

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

关闭循环变量认为有害的

对于foreach循环,你可以这样做,这样就不能用inner-scoped变量编译它们? 或者这只是在匿名方法和lambda表达式可用或者通用的情况下所做的任意选择,并且自此后还没有修改?

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

我认为公平的决定是公平的。 这是最糟糕的"gotchas"在 C# 之一,并我们打算采取这重大更改来修复它。 在 C# 5 内并不限于只变量将逻辑上了循环主体,因此闭包每次都会有个新的副本

for 循环将不会被更改,这里更改将不会被"背面已经移植"到 C# 以前的版本。 因此,在使用这里习惯用法时,你应该继续小心。

你在问什么由埃里克Lippert在他博客文章深入地介绍了关闭这家循环变量被认为有害和它的续集。

对于我来说,最令人信服的参数是每次迭代中的新变量都与 for(;;) 风格循环不一致。 你是否希望在每次迭代中都有一个新的int ifor (int i = 0; i <10; 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 在危难循环中的位置对于在embedded-statement中发生的任何匿名函数捕获它是很重要的。 例如:


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

如果 v 是在危难循环之外声明的,那么它将在所有迭代中共享,它的值for循环后的值将是最后一个值 13,这就是 f的调用。 相反,因为每个迭代都有它自己的变量 v,所以由 f 捕获的第一个迭代将继续保留值 7,这就是将要打印的值。 ( 注意:C#的早期版本声明 v 进入或者重复循环时的外部。 )

...