在使用foreach對異步委託賦值的時候,發現一個問題。代碼以下:c#
static void Main(string[] args) { List<Task> lst_tsk = new List<Task>(); List<int> lst_item = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; foreach (var item in lst_item) { Task tsk = new Task(() => { Console.WriteLine(item); }); lst_tsk.Add(tsk); tsk.Start(); } Console.ReadLine(); }
往Task中,賦值一個拉姆達表達式,期待運行的結果應該是1,2,3,4,5,6,7,8,9,10亂序輸出。可是實際上的結果是10,10,10,10,10,10,10,10,10,10。不少人都認爲這是c#編譯器的一個bug。Eric作出瞭解釋,根據Eric的文章,在foreach循環語句中的變量只有一個item,該變量在循環事後,被賦值爲10了。當異步線程啓動的時候,取到的item早就變成10了,所以就得出上面的結果。閉包
根據Eric的文章,foreach只是一個語法糖,它對應的代碼以下異步
IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator(); try { int m; // OUTSIDE THE ACTUAL LOOP while(e.MoveNext()) { m = (int)(int)e.Current; funcs.Add(()=>m); } } finally { if (e != null) ((IDisposable)e).Dispose(); }
能夠看到m並不包括在while語句中,並且()=>m的意思是返回當前m變量的值,而不是返回委託建立時m變量的值。所以當這個委託真正運行的時候,找到的m可能已是其它值了。ide
若是把語法糖改爲以下的方式:oop
try { while(e.MoveNext()) { int m; // INSIDE m = (int)(int)e.Current; funcs.Add(()=>m); } }
那麼m在while內部,每個m都是單獨的。根據Eric,不這樣改的一個緣由就是,它可能會增長了在循環中使用閉包的次數,(由於異步線程在啓動時,都會用到循環中的m,這個m的生命週期在while循環中,只能經過閉包機制,使得其值可以繼續保留在內存中,可以讓異步委託在調用的時候繼續訪問到該值)。並且,若是這樣修改了,用戶會以爲foreach每個循環都使用了一個新的變量,而不是一個存儲了新值的舊變量。線程
所以,一開始的演示代碼,只須要以下修改既能夠了:blog
static void Main(string[] args) { List<Task> lst_tsk = new List<Task>(); List<int> lst_item = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; foreach (var item in lst_item) { var copy= item;//增長一個臨時的拷貝變量 Task tsk = new Task(() => { Console.WriteLine(copy ); }); lst_tsk.Add(tsk); tsk.Start(); } Console.ReadLine(); }
這樣的話,每次委託運行的時候,都會去找copy 變量了。生命週期
多是不少人的意見影響了C#編譯器團隊,在C#5.0中,他們決定修改這個問題,foreach循環中的變量存在於循環中,所以每次循環都使用的是一個新的變量。for循環暫時不作修正。所以,演示代碼在VS2012下,使用C#5.0的編譯器編譯,獲得的結果是如預期那樣的亂序輸出。ip