我遇到了一个关于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。

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

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


当前回答

由于这里没有人直接引用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();
}

我相信你正在经历的是所谓的关闭http://en.wikipedia.org/wiki/Closure_(computer_science)。你的lamba有一个对变量的引用,这个变量的作用域在函数本身之外。你的lamba直到你调用它才被解释,一旦它被解释,它将在执行时得到变量的值。

这和循环没有关系。

触发此行为是因为您使用了lambda expression() =>变量* 2,其中外部作用域变量实际上没有在lambda的内部作用域中定义。

Lambda expressions (in C#3+, as well as anonymous methods in C#2) still create actual methods. Passing variables to these methods involve some dilemmas (pass by value? pass by reference? C# goes with by reference - but this opens another problem where the reference can outlive the actual variable). What C# does to resolve all these dilemmas is to create a new helper class ("closure") with fields corresponding to the local variables used in the lambda expressions, and methods corresponding to the actual lambda methods. Any changes to variable in your code is actually translated to change in that ClosureClass.variable

所以你的while循环会不断更新ClosureClass。变量,直到它达到10,然后for循环执行操作,这些操作都操作在同一个ClosureClass.variable上。

为了得到预期的结果,您需要在循环变量和被封闭的变量之间创建一个分离。你可以通过引入另一个变量来做到这一点,即:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

你也可以将闭包移动到另一个方法来创建这种分离:

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

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

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

您可以将Mult实现为lambda表达式(隐式闭包)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

或者使用一个实际的helper类:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

在任何情况下,“闭包”都不是一个与循环相关的概念,而是与使用局部作用域变量的匿名方法/ lambda表达式有关——尽管一些不小心使用循环会显示闭包陷阱。

解决这个问题的方法是将需要的值存储在代理变量中,并捕获该变量。

I.E.

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

是的,你需要在循环内的作用域变量,并通过这种方式传递给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();