跳到主要內容

.NET 匿名函數引用局部變量導致的問題

· 閱讀需 2 分鍾

問題

寫C#窗口程序,今天遇到的問題。在工作線程(非UI線程)要操作ListView,因此使用了跨線程調用方式。

for (int i = 0; i < videos.Count; ++i)
{
this.BeginInvoke(new Action(() =>
{
var item = lvwVideos.FindItemWithText(videos[i]);
if (item != null)
lvwVideos.Items.Remove(item);
}));
}

代碼一跑給我拋出了異常,數組下標越界。原因分析如下。

分析

概念理解錯誤,C#的閉包環境並不會復制用到的局部變量。

.NET對閉包的實現是在編譯階段而不是運行階段,事實上,匿名函數中的變量 i 和 for 循環中的i就是同一個變量,由於函數返回後變量還會被匿名函數使用,它會保存在堆中而不是調用棧——不管這個變量是值類型還是引用類型(如果是引用類型即對象和對象的引用都在堆中)。

因此,循環結束後 i 的值已經超出數組 videos 的下標範圍。而BeginInvoke函數是不等待執行完畢的,因此很可能循環結束而匿名函數還沒有執行完畢。這時匿名函數從堆中取到的 i 已經不是定義匿名函數時 i 的值了。

 
用下面的代碼來說可能更清晰。

static void Main(string[] args)
{
int i = 1;
Action action = new Action(() =>
{
Console.WriteLine(i);
});
++i;
action(); // 2
}

action執行的時候訪問的 i 和Main函數中的 i 是同一個,存儲在堆中。

解決

使用Invoke

這裡最簡單的解決方法是將BeginInvoke改為Invoke。Invoke()會等待UI線程執行完畢才會繼續執行,能夠保證 i 值不會被工作線程改變。

模仿JS

將上面的代碼稍作修改:

static void Main(string[] args)
{
int i = 1;
Action action = null;
new Action<int>((n) =>
{
action = new Action(()=>
{
Console.WriteLine(n);
});
})(i);
++i;
action(); // 1
}

這相當於將 i 復制了一遍。