当前位置: 首页 > 科技观察

吃透闭包实现原理

时间:2023-03-22 11:33:59 科技观察

前言对于长期写Java的开发者来说,闭包恐怕很少听说过。在写Python和Go之前,我对它了解不多。光是名字就感觉有点“神秘”,本文的主要目的是从编译器的角度分析闭包,深入理解闭包的实现原理。Functionfirst-classcitizen一个语言在实现闭包之前必须具备的第一个特性:firstclassfunction函数是第一公民。简单的说,函数可以像普通值一样在函数中传递,也可以赋值给变量。我们来看看它在Go中是如何写的:varInner++返回varInner}returninnerFun}funcmain(){varExternal=10f2:=f1()fori:=0;我<2;i++{fmt.Printf("varInner=%d,varExternal=%d\n",f2(i),varExternal)}fmt.Println("======")f3:=f1()fori:=0;我<2;i++{fmt.Printf("varInner=%d,varExternal=%d\n",f3(i),varExternal)}}//输出:0varInner=21,varExternal=111varInner=22,varExternal=12======0varInner=21,varExternal=131varInner=22,varExternal=14这反映了闭包的两个重要特征。第一个自然是函数可以作为值返回,也可以赋值给变量。二是在闭包函数f1()中访问闭包变量varInner。每个闭包函数引用都会在自己的函数中保存一个闭包变量varInner,这样在调用过程中不会互相影响。.从打印结果中也可以看出这一特征。作用域闭包不容易理解的主要原因是它不是很独立。本质上就是作用域的关系。当我们调用f1()函数时,变量varInner将被分配到堆栈上。一般情况下,调用完成后f1的栈会弹出,里面的变量varInner自然会被销毁。正确的。但是在后续的f2()和f3()调用中,仍然可以访问到varInner,这不符合我们对函数调用的直觉。但其实换个角度来看,对于innerFun来说,他可以访问varExternal和varInner两个变量,最外层的varExternal不用说了,肯定是可以访问的。但是对于varInner就不一定了。这里有两种情况;关键点取决于语言是否是静态/动态范围。就静态作用域而言,各个符号的树状关系由编译器决定,在运行时不会改变;也就是说,对于函数innerFun,varInner已经被确定为在编译时可以访问,而在运行时也可以访问。当然,它也是可以访问的。但是对于动态作用域,完全是在运行时来确定正在访问哪个变量。恰好Go是一种具有静态范围的语言,因此返回的innerFun函数始终可以访问varInner变量。实现了闭包,但是在f1()函数退出后,Go怎么还能访问到f1()中的变量呢?这里我们不妨做一个大胆的假设:首先,编译时扫描哪些闭包变量,也就是这里的varInner,需要保存在函数innerFun()中。f2:=f1()f2()需要判断f2是函数,不是变量,同时需要知道它包含的函数体是由innerFun()定义的。然后只要执行函数体的语句即可。而当f3:=f1()重新赋值给f3时,f2中累积的varInner变量不会影响到f3,这就需要在赋值给f3之后,再给f3重新赋一个闭包变量,这样才能达到互不影响的效果。闭包扫描GScript本身也支持闭包,因此Go代码翻译如下:intvarExternal=10;funcint(int)f1(){intvarInner=20;intinnerFun(inta){println(a);intc=100;变量外部++;变量内部++;返回varInner;}returninnerFun;}funcint(int)=f1();for(inti=0;i<2;i++){println("varInner="+f2(i)+",varExternal="+varExternal);}println("========");funcint(int)=f1();for(inti=0;i<2;i++){println("varInner="+f3(i)+",varExternal="+varExternal);}//输出:0varInner=21,varExternal=111varInner=22,varExternal=12=======0varInner=21,varExternal=131varInner=22,varExternal=14,你可以看到运行结果和Go是一样的,那我们看看GScript是怎么实现的,了解一下Go的原理。先看第一步扫描闭包变量:allVariable:=c.allVariable(function)查询所有变量,包括父作用域的变量。scopeVariable:=c.currentScopeVariable(function)查询当前作用域包括所有下级作用域中的变量,这样归约后就可以知道闭包变量,然后将所有闭包变量存储在闭包函数中。闭包赋值后,在返回innerFun时,将闭包变量的数据赋值给变量。闭包函数调用funcint(int)=f1();funcint(int)=f1();这里每赋值一次,f1()的返回函数就会被复制到变量f2/f3中,这样两个包含的闭包变量就不会互相影响了。调用函数变量时,如果判断变量是函数,则直接返回函数。然后直接调用函数。函数式编程接下来,您可以使用Firstclass函数来尝试函数式编程:classTest{intvalue=0;测试(intv){值=v;}intmap(funcint(int){returnf(value);}}intsquare(intv){returnv*v;}intadd(intv){returnv++;}intadd2(intv){v=v+2;返回v;}测试t=Test(100);funcint(int)=square;funcint(int)=add;funcint(int)=add2;println(t.map(s));assertEqual(t.map(s),10000);println(t.map(a));assertEqual(t.map(a),101);println(t.map(a2));assertEqual(t.map(a2),102);这个有点类似于Java中流map函数把函数作为值传递,支持匿名函数后会更像函数式编程,现在必须先定义一个函数变量再传递。另外,GScript中的http标准库还利用了函数是一等公民的特性://标准库:BindroutehttpHandle(stringmethod,stringpath,func(HttpContext)handle){HttpContextctx=HttpContext();handle(ctx);}绑定路由时,句柄是一个函数。使用时,直接传递业务逻辑的句柄即可:func(HttpContext)handle(HttpContextctx){Personp=Person();p.name="abc";println("p.name="+p.name);println("ctx="+ctx);ctx.JSON(200,p);}httpHandle("get","/p",handle);总结总的来说,闭包有以下特点:函数需要被当作一等公民对待。在编译时扫描所有闭包变量。返回闭包函数时,给闭包变量赋值。每次创建一个新的函数变量,都需要将闭包数据拷贝进来,这样闭包变量之间就不会相互影响。调用函数变量时,需要判断为函数,而不是变量。可以在Playground体验使用闭包函数打印斐波那契数列。本文GScript源码相关资源链接:https://github.com/crossoverJie/gscript。游乐场源代码:https://github.com/crossoverJie/gscript-homepage。