c#

位置:IT落伍者 >> c# >> 浏览文章

浅谈C#闭包的相关原理


发布日期:2023年02月26日
 
浅谈C#闭包的相关原理

首先想说明一点虽然有这样那样的不好的心态(比如中文技术书)但总体来说国内的技术人员还是喜欢分享和教导别人的这点我的个人感受和之前在园子里看到的朋友的感受恰恰相反我个人其实国内很多技术网友都是很热心的可能因为语言问题同一个技术热点会稍稍落后国外一些但一些成熟的或者基础的概念都可以找到很细致的中文介绍特别是关于闭包因为它的字面解释确实很绕所以基本所有试图解释这一名词的同学都是尽量用自己认为最通俗易懂的方式来进行讲解闲话扯远了这里我就用C#语言来给大家解释下闭包吧

其实要提到闭包我们还得先提下变量作用域和变量的生命周期

在C#里面变量作用域有三种一种是属于类的我们常称之为field第二种则属于函数的我们通常称之为局部变量还有一种其实也是属于函数的不过它的作用范围更小它只属于函数局部的代码片段这种同样称之为局部变量这三种变量的生命周期基本都可以用一句话来说明每个变量都属于它所寄存的对象即变量随着其寄存对象生而生和消亡对应三种作用域我们可以这样说类里面的变量是随着类的实例化而生同时伴随着类对象的资源回收而消亡(当然这里不包括非实例化的static和const对象)而函数(或代码片段)的变量也随着函数(或代码片段)调用开始而生伴随函数(或代码片段)调用结束而自动由GC释放它内部变量生命周期满足先进后出的特性

那么这里有没有例外呢?

答案是有的不过在提这点之前我还需要给各位另外一个名词都说c#就是MS版本的java这话 可能可以这么说但自之后C#就可以自豪的说它绝非java了这里面委托有很大的功劳如果用过java和C#的人并且尝试过写winform程序时全部手写实现代码的人就会有这样一个感受同样的click事件在java中必须要无端的套个匿名类但在c#中你是可以直接将函数名+=到事件之后而不需要显示写上匿名委托的对象类型的因为编译器会帮你做这部分工作和以后的版本之中微软将委托的用法更是发挥的淋漓精致无论是简洁的Lamda还是通俗易懂的LINQ都是源自委托的

你可能要问委托和我们今天要讲的闭包又有什么关系呢?

我们知道c#java和javascriptrubypython这些语言不同在c#和java的世界里面原子对象就是类(当然还有struct和基本变量)而不是其他语言的函数我们可以实例化一个类实例化一个变量但不可以直接new 一个函数也就是表面上看我们是没办法像js那样将函数进行实例化和传递的这也是为什么直到Java 闭包才被姗姗来迟的加入java特性中但对C#来说这些只是表象我刚学c#的时候看到最多的解释委托的话就是:委托啊就相当于c++里面的函数指针啦这句话虽然笼统但却是有一定道理通过委托特别是匿名委托这层对象的包装我们就可以突破无法将函数当做对象传递的限制了

好像这里还是没讲到闭包和委托的关系好吧我太啰嗦了下面从概念开始讲

闭包其实就是使用的变量已经脱离其作用域却由于和作用域存在上下文关系从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象

好拗口程序员还是用示例来说明更好理解

首先来个最简单的javascript中常常见到的关于闭包的例子:

function f(){

var n=;

return function(){

alert(n);

//

return n;

}

}

var a =f();

alert(a());

这段代码翻译成C#代码就是这样:

public class TCloser

{

public Func<int> T()

{

var n = ;

return () =>

{

ConsoleWriteLine(n);

return n;

};

}

}

class Program{

static void Main(){

var a =new TCloser();

var b = aT();

ConsoleWriteLine(b());

}

}

从上面的代码我们不难看到变量n实际上是属于函数T的局部变量它本来生命周期应该是伴随着函数T的调用结束而被释放掉的但这里我们却在返回的委托b中仍然能调用它这里正是闭包所展示出来的威力因为T调用返回的匿名委托的代码片段中我们用到了n而在编译器看来这些都是合法的因为返回的委托b和函数T存在上下文关系也就是说匿名委托b是允许使用它所在的函数或者类里面的局部变量的于是编译器通过一系列动作(具体动作我们后面再说)使b中调用的函数T的局部变量自动闭合从而使该局部变量满足新的作用范围

因此如果你看中的闭包你就可以像js中那样理解它由于返回的匿名函数对象是在函数T中生成的因此相当于它是属于T的一个属性如果你把T的对象级别往上提升一个层次就很好理解了这里就相当于T是一个类而返回的匿名对象则是T的一个属性对属性而言它可以调用它所寄存的对象T的任何其他属性或者方法包括T寄存的对象TCloser内部的其他属性如果这个匿名函数会被返回给其他对象调用那么编译器会自动将匿名函数所用到的方法T中的局部变量的生命周转期自动提升并与匿名函数的生命周期相同这样就称之为闭合

也许你会说这个返回的委托包含的变量n只是编译器通过某种方式隐藏的对这个委托对象的一个同样对象的赋值吧那么我们再对比下面两个方法:

public class TCloser{ public Func<int> T() { var n = ; Func<int> result = () => { return n; }; n = ; return result; } public dynamic T() { var n = ; dynamic result =new { A = n }; n = ; return result; } static void Main(){ var a = new TCloser(); var b = aT(); var c = aT(); ConsoleWriteLine(b()); ConsoleWriteLine(cA); } } 最后输出结果是什么呢?答案是因为闭包的特性这里匿名函数中所使用的变量就是实际T中的变量与之相反的是匿名对象result里面的A只是初始化时被赋予了变量n的值它并不是n所以后面n改变之后A并未随之而改变这正是闭包的魔力所在

你可能会好本身并不支持函数对象那么这样的特性又是从何而来呢?答案是编译器我们一看IL代码便会明白了

首先我给出c#代码:

public class TCloser { public Func<int> T(){ var n = ; return () => { return n; }; } public Func<int> T(){ return () => { var n = ; return n; }; } } 这两个返回的匿名函数的唯一区别就是返回的委托中变量n的作用域不一样而已T中变量n是属于T而在Tn则是属于匿名函数本身的但我们看看IL代码就会发现这里面的大不同了:

thod public hidebysig instance class [mscorlib]SystemFunc`<int> T() cil managed{

maxstack

locals init (

[] class ConsoleApplicationTCloser/<>c__DisplayClass CS$<>__locals

[] class [mscorlib]SystemFunc`<int> CS$$)

L_: newobj instance void ConsoleApplicationTCloser/<>c__DisplayClass::ctor()

L_: stloc

L_: nop

L_: ldloc

L_: ldcis

L_a: stfld int ConsoleApplicationTCloser/<>c__DisplayClass::n

L_f: ldloc L_: ldftn instance int ConsoleApplicationTCloser/<>c__DisplayClass::<T>b__()

L_: newobj instance void [mscorlib]SystemFunc`<int>::ctor(object native int)

L_b: stloc

L_c: brs L_e

L_e: ldloc

L_f: ret

}

thod public hidebysig instance class [mscorlib]SystemFunc`<int> T() cil managed

{

maxstack

locals init (

[] class [mscorlib]SystemFunc`<int> CS$$)

L_: nop

L_: ldsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate

L_: brtrues L_b

L_: ldnull

L_: ldftn int ConsoleApplicationTCloser::<T>b__()

L_f: newobj instance void [mscorlib]SystemFunc`<int>::ctor(object native int)

L_: stsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate

L_: brs L_b

L_b: ldsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate

L_: stloc

L_: brs L_

L_: ldloc

L_: ret

}

看IL代码你就会很容易发现其中究竟了在T函数对返回的匿名委托构造的是一个类名称为newobj instance void ConsoleApplicationTCloser/<>c__DisplayClass::ctor()而在T则是仍然是一个普通的Func委托只不过级别变为类级别了而已

那我们接着看看T中声明的类c__DisplayClass是何方神圣:

class auto ansi sealed nested private beforefieldinit <>c__DisplayClass extends [mscorlib]SystemObject{

custom instance void [mscorlib]SystemRuntimeCompilerServicesCompilerGeneratedAttribute::ctor()

thod public hidebysig specialname rtspecialname instance void ctor() cil managed{}

thod public hidebysig instance int <T>b__() cil managed{}

field public int n }

看到这里想必你已经明白了在C#中原来闭包只是编译器玩的花招而已它仍然没有脱离NET对象生命周期的规则它将需要修改作用域的变量直接封装到返回的类中变成类的一个属性n从而保证了变量的生命周期不会随函数T调用结束而结束因为变量n在这里已经成了返回的类的一个属性了

看到这里我想大家应该大体上了解闭包的来龙去脉了吧闭包其实和类中其他属性方法是一样的它们的原则都是下一层可以畅快的调用上一层定义的各种设定但上一层则不具备访问下一层设定的能力即类中方法里的变量可以自由访问类中的所有属性和方法而闭包又可以访问它的上一层即方法中的各种设定但类不可以访问方法的局部变量同理方法也不可以访问其内部定义的匿名函数所定义的局部变量

这正是C#中的闭包它通过超越java语言的委托打下了闭包的第一步基础随后又通过各种语法糖和编译器来实现如今在NET世界全面开花的Lamda和LINQ也使得我们能够编写出更加简洁优雅的代码

附:后面是吐槽与上文无关大家可以略过这篇文章其实两年之前在给同事讲C#闭包的时候就有想法整理出来和大家分享了不过因为生活工作或许主要还是自己太懒的原因而拖着没动笔到今天早上看到园友抱怨国内教书育人的氛围才最终决定利用晚上时间把它整理然后放出来我个人认为国内技术圈子的氛围尚可虽然仍然很多浮躁和易怒在圈子里徘徊但我们想想国内IT人的生存空间就容易理解了每天最理想的情况朝的干活晚上加班周末加班这些都是常事而对我们而言只要想写出一些经过细细思考的东西都至少需要个小时以上而且最好中间不要有人来打扰这也就注定我们在白天工作时候很难完全有时间静下来组织语言刨掉这些时间留给我们自己的生活时间又有多少呢?所以我每次看到有园友发表帖子的时间是晚上点甚至更晚都毫不意外

我们并非专业写手也不像国外IT人那样有充足的闲暇时光可以钻研自己的最爱我们赚着他们的零头买着比他们本子价格更贵的笔记本担着比他们更高房价的压力来生活这样的生活条件下我们这些可爱的社区(不仅限于cnblogsjavaeyephpchina等)Geek们仍然如此活跃和热情你还能抱怨什么呢?你要知道你看到的每篇文章(如果是工作人士的话)都是他们晚上从点写到点的生活点滴啊

               

上一篇:.NETFramework版本命名与部署

下一篇:Singleton设计模式的C#实现