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

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

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


当前回答

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

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

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

其他回答

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

这和循环没有关系。

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

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

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

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

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

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