闭包,最早最早接触到这个概念,是在学习JavaScript的回调函数,引出了闭包的概念,博主从Go语言的角度重新审视闭包,还是从JavaScript当初这个源头说起。

1.JavaScript中的闭包

function cal(a,b,callback){
    var res=(a+b)*100;
    return callback(res)
}

cal(1,2,function(res){
    console.log(res)
})

一个函数和对其周围状态(lexical environment,词法环境**)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。**

这是MDN上关于闭包的定义,您理解了吗?

2.C#中的闭包

闭包离不开函数,C#没有返回函数类型的概念,直愣愣的返回函数肯定是不行,但是C#创造性了引入了委托delegate类型

委托类型是一个重要概念,向下指代的是函数或者说方法,向上延伸至事件。

说白了,委托就是一个函数类型,老赵(.net老师)把其称为函数占位符,博主喜欢这个说法。

利用委托,博主也写一个类似于JavaScript的回调函数。

static void Main(string[] args)
{
    var Res = Cal(1, 2);
    Console.WriteLine(Res());

    Cal(1, 2, (int res) =>
        {
            Console.WriteLine(res);
        });
}

static void Cal(int a, int b, Action<int> calAction)
{
    int weight = 100;
    calAction((a + b) * weight);
}

这种形式的回调函数,虽然C#中不爱这么称呼,在一些中间件中被大量使用,用于传递一些配置参数。

用到了Cal函数内部的乘法因子weight,并向调用者暴露了内部运算结果。

3.Go语言中的闭包

在Go语言中,我们将再次简练定义闭包:

闭包=函数+引用环境

函数:没什么说的,在Go语言中,就是一种类型,开发者可以把其视作int64 string等一样的类型。

引用环境:着重要理解一下引用环境,这里博主决定不再讲述概念。直接看一段代码(看完先猜一下输出结果):

package main

func test() []func()  {
    var funs []func()
    for i:=0;i<2 ;i++  {
        funs = append(funs, func() {
            println(&i,i)
        })
    }
    return funs
}

func main(){
    funs:=test()
    for _,f:=range funs{
        f()
    }
}
  • test()函数返回一个函数类型的切片
  • 这个函数功能:
    • 打印for循环中变量i的地址与i的值
  • main函数中遍历这个函数切片,并执行函数
#输出结果
0xc000014018 2
0xc000014018 2

可以看到地址不变,值也不变,而且值都是退出循环的值。

结论一

闭包=函数+引用环境,这里函数的引用环境就是for循环中i变量,但是i变量是在不断变化的,虽然地址没变,但是延迟到真正使用函数时。值已改变(循环完成)。

类似情况在C#中的Lambda表达式捕获了外部变量,然后延迟执行,一样会出现这种情况:

Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
    actions[i] = () =>
    {
        Console.Write(i);
    };
}
foreach (Action a in actions)
{
    a();
}

在另一篇博文【Node.js】匿名函数-闭包-Promise也有阐述。

上面Go语言代码的输出结果,肯定不是我们想要的,既然都循环了,肯定是想让其循环输出,办法来了:

package main

func test() []func() {
	var funs []func()
	for i := 0; i < 2; i++ {
		x := i
		funs = append(funs, func() {
			println(&x, x)
		})
	}
	return funs
}

func main() {
	funs := test()
	for _, f := range funs {
		f()
	}
}
0xc000014018 0
0xc000014020 1

结论二

闭包=函数+引用环境,这里函数的引用环境就是for循环内部x变量,但是x变量不同于i:每一次循环就是一次全新的分配空间,赋值。虽然循环已经退出,但是**引用环境(每次不同的x变量)**依然存在。

我们再来看一个有趣的代码,看完接着先猜一下输出结果

package main

func test(x int) (func(),func())  {
	return func() {
		println(x)
		x+=10
	}, func() {
		println(x)
	}
}

func main()  {
	a,b:=test(100)
	a()
	b()
}
#输出结果
100
110

答案有没有出乎你的意料,如果没有,恭喜您,下面的可以不看了。如果有,那我们将再一次理解一下:

闭包=函数+引用环境

我们从a,b:=test(100)说起:

  • 执行test函数,经过值拷贝,为x变量分配了空间,拷贝了值100
  • 此时第一个函数内部操作打印了x,并做x+=10,x为其引用环境
  • 第二个函数内部打印x,x也为其引用环境
  • a()时,对x进行打印输出:100,并作100+10,110
  • b()时,对x进行打印输出:110

所以综上,x是值拷贝后,开辟出的空间,这时返回的函数,虽然是不同函数,但是却是同一个引用环境

为了更清晰,再把x的地址输出:

package main

func test(x int) (func(), func()) {
	return func() {
			println(x)
			x += 10
			println(&x)
		}, func() {
			println(x)
			println(&x)
		}
}

func main() {
	a, b := test(100)
	a()
	b()
}
#输出结果
100
0xc000014018
110
0xc000014018

4.结论

闭包=函数+引用环境