在C#中循环中的捕获变量

283

我遇到了一个有趣的 C# 问题,我的代码如下所示。

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

我希望它输出0、2、4、6、8。但实际上,它输出了五个10。

看起来是由于所有的动作都引用了一个捕获变量。因此,当它们被调用时,它们都具有相同的输出。

有没有一种方法可以解决这个限制,让每个动作实例都有自己的捕获变量?


16
请参阅Eric Lippert的博客系列:Closing over the Loop Variable Considered Harmful - Brian
11
另外,他们正在修改C# 5的代码,使其能够按照您在 foreach 循环中的预期工作。(破坏性变更) - Neal Tibrewala
4
@Neal:虽然这个例子在C# 5中仍然无法正常工作,因为它仍然输出了五个10。 - Ian Oakes
7
它验证了在C# 6.0(VS 2015)上至今输出了五个10。我怀疑闭包变量的这种行为是否需要更改。“捕获的变量总是在委托实际调用时评估,而不是在变量被捕获时评估”。 - RBT
2
Eric Lippert关于这个主题的博客系列现在在这里(https://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/)和这里(https://ericlippert.com/2009/11/16/closing-over-the-loop-variable-considered-harmful-part-two/)。 - Luke Woodward
显示剩余4条评论
11个回答

-2

由于没有人直接引用ECMA-334,因此在这里说明:

10.4.4.10 For语句

检查具有以下形式的for语句的明确赋值情况:

for (for-initializer; for-condition; for-iterator) embedded-statement

就好像该语句已经写成一样完成了:
{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

在规范中进一步阐述,

12.16.6.3 本地变量的实例化

当执行进入变量作用域时,本地变量被认为已经实例化。

[例如:当调用以下方法时,对于每次循环迭代,本地变量x将被实例化和初始化三次。]

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

然而,将 x 的声明移至循环外部会导致只有一个 x 实例化:
static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

[示例结束] 当未被捕获时,无法观察本地变量实例化的准确频率 - 因为实例化的生命周期是不相交的,每个实例化可以简单地使用同一存储位置。 但是,当一个匿名函数捕获一个局部变量时,实例化的效果变得明显。
[示例:示例]
using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

produces the output:

1
3
5

然而,当将 x 的声明移至循环外部时:
static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

the output is:

5
5
5

请注意,编译器允许(但不是必须的)将三个实例优化为单个委托实例(§11.7.2)。
如果for循环声明了一个迭代变量,则该变量本身被视为在循环外声明。 [例如:因此,如果示例更改为捕获迭代变量本身:
static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

只捕获了迭代变量的一个实例,这会产生以下输出:
3
3
3

哦,是的,我想应该提一下,在C++中不会出现这个问题,因为你可以选择变量是按值还是按引用捕获(参见:Lambda capture)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接