关于go:彻底理解闭包实现原理

4次阅读

共计 3617 个字符,预计需要花费 10 分钟才能阅读完成。

前言

闭包对于一个长期写 Java 的开发者来说预计鲜有耳闻,我在写 PythonGo 之前也是没怎么理解,光这名字感觉就有点 ” 神秘莫测 ”,这篇文章的次要目标就是从编译器的角度来剖析闭包,彻底搞懂闭包的实现原理。

函数一等公民

一门语言在实现闭包之前首先要具备的个性就是:First class function 函数是第一公民。

简略来说就是函数能够像一个一般的值一样在函数中传递,也能对变量赋值。

先来看看在 Go 里是如何编写的:

package main

import "fmt"

var varExternal int

func f1() func(int) int {
    varInner := 20
    innerFun := func(a int) int {fmt.Println(a)
        varExternal++
        varInner++
        return varInner
    }
    return innerFun
}

func main() {
    varExternal = 10
    f2 := f1()
    for i := 0; i < 2; i++ {fmt.Printf("varInner=%d, varExternal=%d \n", f2(i), varExternal)
    }
    fmt.Println("======")

    f3 := f1()
    for i := 0; i < 2; i++ {fmt.Printf("varInner=%d, varExternal=%d \n", f3(i), varExternal)
    }
}

// Output:
0
varInner=21, varExternal=11 
1
varInner=22, varExternal=12 
======
0
varInner=21, varExternal=13 
1
varInner=22, varExternal=14 

这里体现了闭包的两个重要个性,第一个天然就是函数能够作为值返回,同时也能赋值给变量。

第二个就是在闭包函数 f1() 对闭包变量 varInner 的拜访,每个闭包函数的援用都会在本人的函数外部保留一份闭包变量 varInner,这样在调用过程中就不会相互影响。

从打印的后果中也能看出这个个性。

作用域

闭包之所以不太好了解的次要起因是它不太合乎盲目。

实质上就是作用域的关系,当咱们调用 f1() 函数的时候,会在栈中调配变量 varInner,失常状况下调用结束后 f1 的栈会弹出,外面的变量 varInner 天然也会销毁才对。

但在后续的 f2()f3() 调用的时,却仍然能拜访到 varInner,就这点不合乎咱们对函数调用的直觉。

但其实换个角度来看,对 innerFun 来说,他能拜访到 varExternalvarInner 变量,最外层的 varExternal 就不用说了,肯定是能够拜访的。

但对于 varInner 来说就不肯定了,这里得分为两种状况;重点得看该语言是 动态 / 动静 作用域。

就动态作用域来说,每个符号在编译器就确定好了树状关系,运行时不会发生变化;也就是说 varInner 对于 innerFun 这个函数来说在编译期曾经确定能够拜访了,在运行时天然也是能够拜访的。

但对于动静作用域来说,齐全是在运行时才确定拜访的变量是哪一个。

恰好 Go 就是一个动态作用域的语言,所以返回的 innerFun 函数能够始终拜访到 varInner 变量。

实现闭包

但 Go 是如何做到在 f1() 函数退出之后仍然能拜访到 f1() 中的变量呢?

这里咱们无妨大胆假如一下:

首先在编译期扫描出哪些是闭包变量,也就是这里的 varInner,须要将他保留到函数 innerFun() 中。

f2 := f1()
f2()

运行时须要判断出 f2 是一个函数,而不是一个变量,同时得晓得它所蕴含的函数体是 innerFun() 所定义的。

接着便是执行函数体的 statement 即可。

而当 f3 := f1() 从新赋值给 f3 时,在 f2 中累加的 varInner 变量将不会影响到 f3,这就得须要在给 f3 赋值的从新赋值一份闭包变量到 f3 中,这样便能达到互不影响的成果。

闭包扫描

GScript 自身也是反对闭包的,所以把 Go 的代码翻译过去便长这样:

int varExternal =10;
func int(int) f1(){
    int varInner = 20;
    int innerFun(int a){println(a);
        int c=100;
        varExternal++;
        varInner++;
        return varInner;
    }
    return innerFun;
}

func int(int) f2 = f1();
for(int i=0;i<2;i++){println("varInner=" + f2(i) + ", varExternal=" + varExternal);
}
println("=======");
func int(int) f3 = f1();
for(int i=0;i<2;i++){println("varInner=" + f3(i) + ", varExternal=" + varExternal);
}

// Output:
0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
=======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14

能够看到运行后果和 Go 的一样,所以咱们来看看 GScript 是如何实现的便也能了解 Go 的原理了。


先来看看第一步扫描闭包变量:

allVariable := c.allVariable(function)
查问所有的变量,包含父 scope 的变量。

scopeVariable := c.currentScopeVariable(function)
查问以后 scope 蕴含上级所有 scope 中的变量,这样一减之后就能晓得闭包变量了,而后将所有的闭包变量寄存进闭包函数中。

闭包赋值

之后在 return innerFun 处,将闭包变量的数据赋值到变量中。

闭包函数调用

func int(int) f2 = f1();

func int(int) f3 = f1();

在这里每一次赋值时,都会把 f1() 返回函数复制到变量 f2/f3 中,这样两者所蕴含的闭包变量就不会相互影响。


在调用函数变量时,判断到该变量是一个函数,则间接返回函数。

之后间接调用该函数即可。

函数式编程

接下来便能够利用 First class function 来试试函数式编程:


class Test{
    int value=0;
    Test(int v){value=v;}

    int map(func int(int) f){return f(value);
    }
}
int square(int v){return v*v;}
int add(int v){return v++;}
int add2(int v){
    v=v+2;
    return v; 
}
Test t =Test(100);
func int(int) s= square;
func int(int) a= add;
func int(int) a2= 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 规范库也利用了函数是一等公民的个性:

// 规范库:Bind route
httpHandle(string method, string path, func (HttpContext) handle){HttpContext ctx = HttpContext();
    handle(ctx);
}

在绑定路由时,handle 便是一个函数,应用的时候间接传递业务逻辑的 handle 即可:

func (HttpContext) handle (HttpContext ctx){Person p = 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
  • Playground 源码:https://github.com/crossoverJie/gscript-homepage
正文完
 0