2008-11-07 37 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; 
    } 
} 

這不是真正從樣本的編譯代碼,但我已經檢查了我自己的代碼,這看起來非常比如編譯器實際上會產生什麼。