C#在foreach中重用变量是否有原因?

StriplingWarrior 1569 c# foreach lambda scope anonymous-methods

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

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这种方式处理循环,如果它们是使用内部范围的变量编译的话,你可以做到这一点,或者这只是在匿名方法和lambda表达式可用或普通之前做出的任意选择,并且没有从那时起被修改过?

Eric Lippert.. 1332

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

你的批评是完全合理的.

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

关闭循环变量被认为是有害的

有没有东西你可以用foreach循环这样做,如果它们是用内部范围的变量编译你不能?或者这只是在匿名方法和lambda表达式可用或常见之前做出的任意选择,并且从那以后还没有修改过?

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

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

for循环将不会改变,并且改变将不会被"向后移植"到C#的早期版本.因此,在使用这个习语时你应该继续小心.

  • 事实上我们确实在C#3和C#4中推迟了这一变化.当我们设计C#3时,我们确实意识到问题(已经存在于C#2中)会变得更糟,因为会有这么多的lambda(和查询)由于LINQ,在foreach循环中的理解,这是伪装的lambda.我很遗憾我们等待这个问题变得非常糟糕,以至于不能在很晚的时候修复它,而不是在C#3中修复它. (170认同)
  • 而现在我们必须记住`foreach`是'安全',但`for`不是. (72认同)
  • @Benjol:不,这就是为什么我们愿意接受它.Jon Skeet向我指出了一个重要的突破性变化场景,即有人在C#5中编写代码,对其进行测试,然后与仍在使用C#4的人分享,然后他们天真地认为它是正确的.希望受这种情况影响的人数很少. (39认同)
  • 顺便说一句,ReSharper总是抓住这个并将其报告为"访问修改后的封闭".然后,通过按Alt + Enter,它甚至会自动为您修复代码.http://www.jetbrains.com/resharper/ (27认同)
  • @michielvoo:这种变化在不向后兼容的意义上是突破性的.使用较旧的编译器时,新代码无法正常运行. (21认同)
  • @CodeInChaos:我们不能把它变成编译器*error*来编写一个在循环变量上关闭的lambda,因为那时你不能在foreach循环中使用LINQ!治愈将比疾病严重得多. (8认同)
  • @ Random832:不太可能.然而,我们正在考虑为Roslyn添加一个静态分析器,该分析器确定在构造闭包之后是否写入了封闭变量; 如果不是,那么我们可以关闭值而不是变量. (7认同)
  • 我不会把它称为*破坏性更改*,而是*启用更改*:曾经巧妙错误的代码现在将开始按预期运行.当然,有些人可能依赖旧的行为...... (7认同)
  • @EricLippert,出于好奇,你们是否设法想出任何不完全古怪的场景,这些场景可能会被这种"破坏性"变化打破? (7认同)
  • 我想我会把它改成编译器错误.巧妙地改变语义对我来说太危险了.我想知道有多少代码依赖于旧的行为. (6认同)
  • 实际上,1.x规范中有间接引用; 如果你看一下明确的赋值规则,它给出了一个编译器解释的例子,而IIRC它在**循环中被声明为**.不过,这是间接的和间接的.不是明确的规范声明. (5认同)
  • @usr在这里一样; 但是如果没有安全/不安全的工具,我选择几乎总是使用显式温度.创建错误的方法太多,无法在混合中添加微妙的错误. (2认同)
  • 我认为当前的行为是可以理解的,因为lambda将在*循环被评估之后(或在评估的各个阶段)进行评估,但它不直观.谢谢你改变它. (2认同)
  • 哪个VB.Net版本会更正? (2认同)

Krizz.. 179

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

对我来说,最有说服力的论点是在每次迭代中使用新变量将与for(;;)样式循环不一致.你期望int i在每次迭代中有一个新的for (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变量.

  • 最终,人们在写这篇文章时实际上_want_并不是有多个变量,而是要关闭_value_.在一般情况下,很难想到可用的语法. (18认同)
  • 在C#中关闭引用的关闭太糟糕了.如果它们默认关闭了值,我们可以使用`ref`轻松指定关闭变量. (6认同)
  • @Krizz,这是强制一致性比不一致更有害的情况.它应该像人们期望的那样"正常工作",并且显然人们期望在使用foreach而不是for循环时会有不同的东西,因为在我们知道访问修改后的闭包问题之前遇到问题的人数(比如我自己) . (2认同)
  • @ Random832不了解C#,但在Common LISP中有一个语法,它表明任何具有可变变量和闭包的语言(nay,*must*)也都有.我们要么关闭对变化的地方的引用,要么关闭它在给定时刻的价值(创建一个闭包).[This](http://www.phyast.pitt.edu/~micheles/scheme/scheme14.html)讨论Python和Scheme中的类似内容(`cut`用于refs/vars和`cute`用于保持*evaluate*值在部分评估的闭包中). (2认同)

Godeke.. 98

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

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.

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

  • 这是典型的解决方法感谢您的贡献.Resharper非常聪明,可以识别这种模式并引起你的注意,这很好.我有一段时间没有被这种模式所困扰,但是因为它是Eric Lippert的话,"我们得到的最常见的错误报告",我很想知道*为什么*比*怎么样躲开它*. (22认同)

Paolo Morett.. 56

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

(......)

vwhile循环内部的放置对于嵌入式语句中发生的任何匿名函数如何捕获它非常重要.例如:

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

  • @colinfang一定要阅读[Eric的回答](http://stackoverflow.com/a/8899347/63011):C#1.0规范(_在你的链接中我们谈论的是VS 2003,即C#1.2_)实际上**做了不要说**循环变量是在循环体内部还是外部,因为它没有任何可观察到的差异.当在C#2.0中引入闭包语义时,选择将循环变量放在循环之外,与"for"循环一致. (4认同)
  • @colinfang他们是明确的规范.问题是我们正在讨论稍后介绍的一个功能(即函数闭包)(使用C#2.0).当C#2.0出现时,他们决定将循环变量放在循环之外.而且他们用C#5.0再次改变主意:) (4认同)