.NET 匿名函数引用局部变量导致的问题
程序员文章站
2022-03-08 23:28:34
...
问题
写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 复制了一遍。
上一篇: 04_10.字符串的扩展方法.js
下一篇: 匿名函数、函数作为变量