欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

.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 复制了一遍。

相关标签: .NET 匿名函数