协慌网

登录 贡献 社区

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

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

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

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

正如这里所解释的那样,这是因为在上面的foreach循环中声明的s变量在编译器中被翻译成这样:

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 表达式可用或普通之前做出的任意选择,以及从那以后还没有修改过?

答案

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

你的批评是完全合理的。

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

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

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

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

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

for循环不会被更改,并且更改不会 “反向移植” 到以前版本的 C#。因此,在使用这个习语时你应该继续小心。

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

对我来说,最有说服力的论点是在每次迭代中使用新变量将与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

我的博客文章关于这个问题: 关闭 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.

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