我遇到了一个关于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。然而,它实际上输出了5个10。

这似乎是由于所有的操作都指向一个捕获的变量。因此,当调用它们时,它们都有相同的输出。

有没有办法绕过这个限制,让每个动作实例都有自己的捕获变量?


当前回答

for (int n=0; n < 10; n++) //forloop syntax
foreach (string item in foo) foreach syntax

其他回答

它被称为闭合问题, 只需使用一个复制变量,就完成了。

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

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

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

由于这里没有人直接引用ECMA-334:

10.4.4.10用于语句 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();
  }
}

产生输出:

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;
}

输出结果为:

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捕获)。

同样的情况也发生在多线程中(c#, . net 4.0)。

请看下面的代码:

目的是按顺序打印1,2,3,4,5。

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

输出很有趣!(可能是21334年……)

唯一的解决方案是使用局部变量。

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

是的-在循环中获取变量的副本:

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

你可以把它想象成c#编译器每次碰到变量声明时都会创建一个“新的”局部变量。事实上,它会创建适当的新闭包对象,如果你在多个作用域中引用变量,它会变得复杂(在实现方面),但它是有效的:)

注意,这个问题更常见的用法是for或foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

有关这方面的更多细节,请参阅c# 3.0规范的7.14.4.2节,我关于闭包的文章也有更多示例。

请注意,从c# 5编译器开始(甚至在指定较早版本的c#时),foreach的行为发生了变化,因此您不再需要进行本地复制。更多细节请看这个答案。

是的,你需要在循环内的作用域变量,并通过这种方式传递给lambda:

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

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

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

Console.ReadLine();