2008-11-07 82 views
154

我遇到了一个关于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,但是,它实际上输出5分10秒。

看来,这是由于所有操作引用一个捕获变量。结果,当它们被调用时,它们都具有相同的输出。

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

+9

另请参见Eric Lippert关于此主题的博客系列:[关闭循环变量被视为有害](http://blogs.msdn.com/b/ericlippert/archive/tags/closures/) – Brian 2010-11-11 21:50:50

+6

此外,他们正在改变C#5按照您的预期在foreach内工作。 (突破变化) – 2012-03-04 18:55:00

+0

相关:[为什么这是一个不好用的迭代变量在一个lambda表达式](http://stackoverflow.com/questions/227820/why-is不好使用迭代变量的lambda表达式) – nawfal 2013-11-02 07:08:51

回答

137

是 - 把变量的副本内循环:

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

你可以把它看作如果C#编译器每次碰到变量声明时创建一个“新”的局部变量。实际上,它会创建适当的新闭合对象,并且如果引用多个范围中的变量,它会变得复杂(在实现方面),但它会起作用:)

请注意,此问题更常见的情况是使用forforeach

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,我的article on closures有更多的例子太多。

4

是的,你需要的范围variable内环路,并把它传递给拉姆达这样:

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

解决这个问题的方法是存储你的代理变量需要,并具有值变量GET抓获。

I.E.

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

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

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

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

这不是真正从样本的编译代码,但我已经检查了我自己的代码,这看起来非常比如编译器实际上会产生什么。