ChatGPT解决这个技术问题 Extra ChatGPT

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

在 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 变量的引用(在循环外部内部声明),因此您最终会与它的最终值进行比较;我明白了。我不明白的是,在循环内声明变量会有什么不同。从编译器编写者的角度来看,无论声明是在循环内部还是外部,我都只在堆栈上分配一个字符串引用(var's');我当然不想每次迭代都将新引用推送到堆栈上!

E
Eric Lippert

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

你的批评完全有道理。

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

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 它是在循环内声明的。不过,这是间接的和间接的。不是明确的规范声明。
事实上,我们确实在 C# 3 和 C# 4 中推迟了这种变化。当我们设计 C# 3 时,我们确实意识到问题(C# 2 中已经存在)会变得更糟,因为会有太多的 lambda(和查询由于 LINQ,foreach 循环中的推导式,它们是伪装的 lambda。我很遗憾我们一直等到问题变得严重到需要这么晚才修复它,而不是在 C# 3 中修复它。
现在我们必须记住 foreach 是“安全的”,但 for 不是。
K
Krizz

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#


最终,人们在写这篇文章时真正想要的不是拥有多个变量,而是关闭值。在一般情况下,很难想到可用的语法。
M
Mohammad Albay

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

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 的话来说,它是“我们得到的最常见的错误错误报告”,我很想知道为什么而不是如何避免它。
P
Paolo Moretti

在 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。)


为什么这个早期版本的 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 改变了主意 :)