您好,登錄后才能下訂單哦!
在使用foreach對異步委托賦值的時候,發現一個問題。代碼如下:
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可能已經是其它值了。
如果把語法糖改成如下的方式:
try { while(e.MoveNext()) { int m; // INSIDE m = (int)(int)e.Current; funcs.Add(()=>m); } }
那么m在while內部,每一個m都是單獨的。根據Eric,不這樣改的一個原因就是,它可能會增加了在循環中使用閉包的次數,(因為異步線程在啟動時,都會用到循環中的m,這個m的生命周期在while循環中,只能通過閉包機制,使得其值能夠繼續保留在內存中,能夠讓異步委托在調用的時候繼續訪問到該值)。而且,如果這樣修改了,用戶會覺得foreach每一個循環都使用了一個新的變量,而不是一個存儲了新值的舊變量。
因此,一開始的演示代碼,只需要如下修改既可以了:
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的編譯器編譯,得到的結果是如預期那樣的亂序輸出。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。