【问题标题】:Is there a reason for C#'s reuse of the variable in a foreach?C# 在 foreach 中重用变量是否有原因?
【发布时间】:2016-01-15 00:28:29
【问题描述】:

在 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) { ... } 有什么问题?
  • @BradChristie OP 并不是真正在谈论 foreach,而是在谈论导致类似代码的 lamda 表达式,如 OP 所示...
  • @BradChristie:可以编译吗? (错误:foreach 语句中需要类型和标识符对我来说)
  • @JakobBotschNielsen:它是 lambda 的封闭外部局部;你为什么假设它会在堆栈上?它的生命周期比栈帧长!
  • @EricLippert:我很困惑。我知道 lambda 捕获对 foreach 变量的引用(在循环内部声明 outside),因此您最终会与它的最终值进行比较;我明白了。我不明白的是,在循环中声明变量 inside 会有什么不同。从编译器编写者的角度来看,无论声明是在循环内部还是外部,我都只在堆栈上分配一个字符串引用(var's');我当然不想每次迭代都将新的引用推入堆栈!

标签: c# foreach lambda scope anonymous-methods


【解决方案1】:

受此困扰,我习惯于将本地定义的变量包含在最内层范围内,我用它来转移到任何闭包。在您的示例中:

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 的话来说,它是“我们得到的最常见的错误错误报告”,我很想知道 为什么 更多而不是如何避免它。
【解决方案2】:

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#。

【讨论】:

  • 最终,人们在写这篇文章时真正想要的不是有多个变量,而是关闭值。在一般情况下,很难想出一个可用的语法。
【解决方案3】:

编译器声明变量的方式使其极易出现通常难以发现和调试的错误,同时不会产生明显的好处。

您的批评完全有道理。

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

Closing over the loop variable considered harmful

是否可以通过这种方式对 foreach 循环执行某些操作,而如果它们是使用内部作用域变量编译的,您就无法做到这一点?或者这只是在匿名方法和 lambda 表达式可用或常见之前做出的任意选择,并且从那时起就没有修改过?

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

我认为可以公平地说,所有人都对这个决定感到遗憾。这是 C# 中最糟糕的“陷阱”之一,我们将采取重大更改来修复它。在 C# 5 中,foreach 循环变量将在逻辑上在内部循环体,因此闭包每次都会得到一个新的副本。

for 循环不会更改,更改不会“向后移植”到以前的 C# 版本。因此,您在使用此成语时应继续小心。

【讨论】:

  • 那么,语法没有机会关闭值? (当然,这有一个“没人会使用它”的问题,因为关闭变量的语法更自然,90% 的时间没有区别)
  • @Random832:不太可能。然而,我们正在考虑为 Roslyn 添加一个静态分析器,以确定在构造闭包之后是否曾经写入封闭变量;如果不是,那么我们可以关闭值而不是变量。
  • 其实1.x规范中有间接引用;如果您查看明确的分配规则,它会给出编译器解释的示例,并且 IIRC 它被声明为 inside 循环。不过,这是间接的和间接的。不是明确的规范声明。
  • 事实上,我们确实在 C# 3 和 C# 4 中推动了这一变化。当我们设计 C# 3 时,我们确实意识到问题(在 C# 2 中已经存在)会变得更糟,因为存在由于 LINQ,foreach 循环中会有这么多的 lambdas(和查询理解,它们是伪装的 lambdas)。很遗憾,我们等到问题变得严重到需要这么晚才修复它,而不是在 C# 3 中修复它。
  • 现在我们必须记住 foreach 是“安全的”,但 for 不是。
【解决方案4】:

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

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

【讨论】:

  • 为什么这个早期版本的 C# 在 while 循环中声明 v?msdn.microsoft.com/en-GB/library/aa664754.aspx
  • @colinfang 请务必阅读Eric's answer:C# 1.0 规范(在您的链接中我们正在讨论 VS 2003,即 C# 1.2)实际上没有说循环变量是在循环体内部还是外部,因为它没有明显的区别。在 C# 2.0 中引入闭包语义时,选择将循环变量放在循环之外,与“for”循环一致。
  • 所以您是说链接中的示例当时还不是最终规范?
  • @colinfang 它们是明确的规范。问题是我们谈论的是后来(使用 C# 2.0)引入的特性(即函数闭包)。当 C# 2.0 出现时,他们决定将循环变量放在循环之外。然后他们又用 C# 5.0 改变了主意 :)
猜你喜欢
  • 1970-01-01
  • 2012-08-20
  • 2011-02-27
  • 2015-11-01
  • 2013-05-28
  • 2020-05-05
  • 2021-06-20
相关资源
最近更新 更多