提问者:小点点

C#循环中捕获的变量


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

似乎是由于所有的动作都引用了一个捕获的变量。因此,当它们被调用时,它们都具有相同的输出。

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


共3个答案

匿名用户

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

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

您可以把它想像成C#编译器每次命中变量声明时都会创建一个“新的”局部变量。实际上,它将创建适当的新闭包对象,如果引用多个作用域中的变量,则会变得复杂(就实现而言),但它可以工作:)

请注意,更常见的此问题是使用foreachforeach:

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的行为发生了变化,因此您不再需要进行本地复制。请参阅此答案以了解更多详细信息。

匿名用户

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

匿名用户

在幕后,编译器正在生成一个表示方法调用的闭包的类。它将closure类的单个实例用于循环的每次迭代。代码如下所示,这样更容易看出bug发生的原因:

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

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

相关问题