一尘不染

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

c#

在 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 表达式可用或常见之前做出的任意选择,并且没有从那以后就没有修改过?


阅读 128

收藏
2022-02-16

共1个答案

一尘不染

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

你的批评完全有道理。

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

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

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

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

2022-02-16