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

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

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


当前回答

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

其他回答

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

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

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

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

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

I.E.

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