前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编码技巧 --- 谨防闭包陷阱

编码技巧 --- 谨防闭包陷阱

作者头像
Niuery Diary
发布2023-10-22 16:59:44
1480
发布2023-10-22 16:59:44
举报
文章被收录于专栏:Niuery的技术日记

引言

先不论什么是闭包,什么是闭包陷阱,我们开篇先看一段代码:

代码语言:javascript
复制
static void Main(string[] args)
{
    List<Action> lists = new List<Action>();

    for (int i = 0; i < 5; i++)
    {
        Action action = () => { Console.WriteLine(i); };

        lists.Add(action);
    }

    foreach (var action in lists)
    {
        action();
    }

    Console.ReadLine();
}

那么思考一下,控制台输出是什么?

闭包陷阱

上述代码的本意是想让 Action声明的匿名委托方法接收 i 的值,并输出

代码语言:javascript
复制
0
1
2
3
4

但实际上,上述代码输出的是

代码语言:javascript
复制
5
5
5
5
5

为什么会这样?就需要提到两个个概念,「变量作用域」「闭包」

「变量作用域就不说了,就是指变量可以被访问的范围。例如全局变量,局部变量等」

「闭包:简单点说,就是函数和其引用的上下文的组合体,就是一个闭包。」,例如上文代码中,for 循环内部,匿名方法内引用了变量 i ,那么变量 i 和匿名方法 () => { Console.WriteLine(i); } 就组合成了一个闭包,在 for 循环中,变量 i 就是一个局部变量,但是在闭包中,变量 i 对于匿名方法来说就是全局变量。相当于这样:

代码语言:javascript
复制
int i;

public void AnonymousMethod()
{
    Console.WriteLine(i);
}

所以,当 for 循环结束时,在闭包内的全局变量 i 的值就已经变成了5。则下面 foreach 代码每次执行输出均为5。

根据IL探究原理

实际上,编译器在执行的时候,也确实为闭包生成了一个类,这个类只包含了一个方法和一个全局变量。

来验证一下,将上述代码编译为dll后,通过ILDasm.exe工具查看生成的IL代码

image.png

可以看到IL为闭包生成了一个类 <>c_DisplayClass0_0 ,这个类只包含了一个变量 i,如下:

image.png

还包含了一个匿名方法<Main>b_0:void,从IL中,也可以看出,先取 <>c_DisplayClass0_0 的变量 i ,再调用控制台输出方法。如下:

image.png

接下来,在看一下整个控制台程序 Main 方法的IL代码:

image.png

在IL_0007行,可以看到,声明创建一个 Action 后就紧接着创建了一个 <>c_DisplayClass0_0 对象。且 for 循环的变量 i 就是 <>c_DisplayClass0_0 对象的变量 i (注意这里指的是引用,并不是值),这样,在循环结束的时候,对象的变量 i 就变成了5。

这样就可以解释为什么输出会是5了。

需要注意的是,Action action = () => { Console.WriteLine(i); };这句代码只是声明了一个委托,委托绑定的是一个匿名方法,并没有真正执行,只有调用该委托的时候才真正执行。比如 action()action.Invoke()

如何避免闭包陷阱

在上面的探究原理的过程中,其实也发现了追根究底的问题其实就是,在创建闭包对象的时候,引用的局部变量,在外部被修改(比如上面代码中的for 循环的变量 i 就是闭包对象的变量 i,指的是指针是同一个),那么可以在创建闭包对象的时候,重新创建一个指针对象,将预期值赋值给它就可以了,比如上面的示例代码可以这样修改:

代码语言:javascript
复制
static void Main(string[] args)
{
    List<Action> lists = new List<Action>();

    for (int i = 0; i < 5; i++)
    {
        int temp = i;
        Action action = () => { Console.WriteLine(temp); };
        lists.Add(action);
    }

    foreach (var action in lists)
    {
        action();
    }

    Console.ReadLine();
}

这样,闭包对象的变量就不再是 for 循环的 i 了,也就不会再被修改。输出结果为:

代码语言:javascript
复制
0
1
2
3
4
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-05-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Niuery Diary 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 闭包陷阱
  • 根据IL探究原理
  • 如何避免闭包陷阱
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档