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

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

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


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

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();

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

I.E.

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

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


同样的情况也发生在多线程中(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();
}

在幕后,编译器生成一个表示方法调用的闭包的类。它为循环的每次迭代使用闭包类的单个实例。代码看起来是这样的,这使得它更容易看到错误发生的原因:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

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

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

这实际上不是示例中的编译代码,但我检查了自己的代码,这看起来非常像编译器实际生成的代码。


这和循环没有关系。

触发此行为是因为您使用了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表达式有关——尽管一些不小心使用循环会显示闭包陷阱。


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

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


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