【c#】c#使用匿名函数产生的闭包陷阱

问题起因

在WinForm中,有2对3个功能一样的comboBox,左边三个控件需要监听选项变化,从而改变右边三个控件的选项内容,如下图所示。

【c#】c#使用匿名函数产生的闭包陷阱_第1张图片

为了偷懒减少冗余代码,将两边的三个控件分别赋值给两个ComboBox数组,并使用一个for循环依次给左边的每个comboBox设置监听函数。我的想法是,先在监听函数获取当前选项变化的控件在数组中位置,根据这个下标改变右边数组中对应位置控件的内容。一步一步来,我先在监听函数中弹出一个对话框显示这个位置对不对。代码如下:

【c#】c#使用匿名函数产生的闭包陷阱_第2张图片

代码中我们在for循环里使用lambda表达式给控件增加响应事件:每个控件都会在选中时输出自己对应的索引,这一切看似都很正确,直到我们开始调试程序...

【c#】c#使用匿名函数产生的闭包陷阱_第3张图片

我惊奇的发现无论是哪个控件的变化显示的i都等于3,随后由于下面那句代码,解释器就会抛出索引越界的异常。

问题分析

仔细分析代码,欸,这熟悉的函数写法,这不就是JavaScript里的闭包吗!之前觉得这两门语言十分像(例如async函数和lambda表达式),没想到连匿名函数的闭包问题也如出一辙。想到这,于是很快明白出错的地方。根据js的经验,我猜想在.net执行到我们的匿名函数代码块时,运行环境会将代码块内用到的外部变量进行捕获形成闭包(这个例子中外部变量i被捕获),即在匿名函数内部可以访问到外部的变量。于是生成的三个监听函数并不是 () => { 0 }; () => { 1 }; () => { 2 };而是() => { i }; () => { i }; () => { i }; 因此在函数真正被调用时,i的值为此时i的值,而不是创建时的0,1,2。

经查阅后得知,c#实现匿名函数的方式为编译器为闭包生成了一个类,被捕获的上下文变量在新类中是一个public类型的字段。在之后,即使for循环结束,由于这个类的信息一直存在于堆中,因此这些被捕获的变量会一直存在于内存中,不会消亡,并被会被闭包函数引用。

为了进行验证,在for循环之后改变i的值,猜测此时监听函数中变量i也会响应改变。代码和结果如下:

【c#】c#使用匿名函数产生的闭包陷阱_第4张图片

【c#】c#使用匿名函数产生的闭包陷阱_第5张图片

不过,我还是有个疑问,即使新生成了类,作为字段的i在新生成的类中为Int32类型,并没有进行装箱操作,那么为什么i在循环变化后依然能获取到运行时当前的i呢?个人猜测,在生成新类后,编译器自动地将闭包函数中获取到对应的变量在我们的源上下文代码中找出。在创建这个变量之前生成新类的实例,并对在创建之后的代码块中所有使用该捕获的变量之处进行替换,替换成为新类实例中的字段。

为了验证,我尝试着将i转移到更大的作用范围,即将i作为整个窗体的一个局部变量,并且在额外的按钮点击中改变i的值,然后观察当前闭包的情况,结果符合,说明新类的实例化在捕获到的变量创建之前。代码和运行结果如下:

【c#】c#使用匿名函数产生的闭包陷阱_第6张图片

【c#】c#使用匿名函数产生的闭包陷阱_第7张图片

问题解决

我们在for循环中(匿名函数外)使用中间变量存储下i的值,然后把对i的访问转变为对index的访问。代码如下:

【c#】c#使用匿名函数产生的闭包陷阱_第8张图片

有人可能会说,你个笨比,你这样不还是把index给捕获进去给新类的字段了呐,有啥区别。别急,根据上面的猜测,初始化新类的地方是在创建该捕获到的对象之前一句,并且会替换上下文中所有该对象。

如果不使用index作为中间变量,那么捕获到的变量为i,而i的作用范围为整个for循环,闭包函数内部i会随着外部的i变化,for循环终止时,i为3,所以闭包函数在被调用时的i也为3。

我们的index创建的地方是在for循环内部,所以新类实例化的地方也在其内部,每轮循环都会实例化出一个新类对象,实例化完成时,新类对象中的字段仅为当轮循环拿到的i,而且该轮循环中临时变量index被替换成为了该对象的字段。总之这里闭包函数捕获到的只是个作用域更小的临时变量,所以函数内的index只和当轮拿到的index一致,当下一轮循环开始时,重新声明了变量index,这和上轮的index已经没有了关系,示意图如下:

【c#】c#使用匿名函数产生的闭包陷阱_第9张图片

这种方法的本质就是降低闭包函数捕获到变量的作用范围。

结果符合猜想。所以避免闭包陷阱的关键在于,时刻注意匿名函数内部所使用到的外部变量的作用域。

【c#】c#使用匿名函数产生的闭包陷阱_第10张图片

本文作者为CSDN用户--严则安,关于个人猜测的部分错误难免,欢迎指出。

你可能感兴趣的:(【c#】c#使用匿名函数产生的闭包陷阱)