闭包的概念
闭包(Closure)的概念总是存在于各种支持函数式编程的语言中。首先理解什么是闭包,这里取JavaScript文档中对闭包的定义:
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包。
其中包括两个要点:
- 函数
- 周围环境(&状态&上下文)
函数在Kotlin中主要包括三种形式:普通的具名函数,匿名函数,lambda表达式。周围环境应该如何理解呢,可以理解为函数所处的外部作用域中定义的各个自由变量,这个作用域可能是另一个函数或块级作用域。二者捆绑在一起构成的闭包使得函数内部可以对外部作用域定义的变量进行访问。
不完整闭包的Java
Java中的闭包不同于多数其他函数式编程语言,很多时候不被认为是完整的。
protected void onCreate(Bundle savedInstanceState) {
...
int num = 0;
btn.setOnClickListener(v -> {
num++;//报错
});
}
上面的写法会被IDE提示:Variable used in lambda expression should be final or effectively final。因为根据Java规定:匿名内部类内部,方法或块级作用域内的具名内部类内部使用的外部变量必须是final的。
为什么会有这种规定呢,因为对于Java闭包中的函数而言,周围环境(外部函数或块级作用域)在执行到结束时就会销毁,这里的周围环境对应与虚拟机栈中的一个栈帧。而栈帧中的局部变量,在方法返回后就会被虚拟机回收。
正因为如此,Java中的闭包其实并不完整。从闭包的定义来理解,Java函数中对周围环境中变量的引用无法阻止周围环境的销毁。从Java闭包的实现来看,这种对周围环境中变量的引用只是一种假象,编译器会对使用到的周围环境中的变量拷贝一份副本,而函数内部操作的只是不可见的副本而已。对这份不可见副本的修改不仅毫无意义,还会引起歧义,误以为周围环境中的变量真的被修改。因此,Java干脆从语言层面禁止了这种修改,也就有了上述规定。
绕过Java语法限制
这种因Java中不完整闭包导致的限制在实际开发中有诸多不便,因此常见到长度为1的数组这种技巧绕过限制:
protected void onCreate(Bundle savedInstanceState) {
...
int[] num = new int[]{1};
btn.setOnClickListener(v -> {
num[0]++;//正确
});
}
因为数组对象被分配到堆内存中,原来函数所需的,外部环境中的存在于栈帧中的变量到了堆内存中,会因函数内部引用而不被销毁。
实际上,Java中这种会被内部函数所修改的变量,更多时候我们会把它作为一个类的属性,而不是放在函数或块作用域的内部而受到栈帧出栈的影响:
int num = 0;
protected void onCreate(Bundle savedInstanceState) {
...
btn.setOnClickListener(v -> {
num++;//正确
});
}
换个角度看,如果我们把闭包中周围环境(外部函数或块级作用域)的概念扩展一下,把类中定义的属性也做为函数所处的周围环境的话,那么类本身就可以认为是一个广义的闭包。
另外,Java中内部类,因为存在一个指向外部对象引用,可以自由访问外部类中的内容,也可以认为是一个广义的闭包。
Kotlin闭包的实现
Kotlin中相较于Java,闭包是完整的,并没有需要外部变量需要声明为final的规定。其实现就是将函数使用到的周围环境中的变量,包装为一个新类中的属性。
override fun onCreate(savedInstanceState: Bundle?) {
...
var num = 0;
btn.setOnClickListener {
num++ //正确
}
}
上述代码实际会编译为等价于Java中的下述代码:
protected void onCreate(Bundle savedInstanceState) {
...
final Ref.IntRef num = new Ref.IntRef();
num.element = 0;
btn.setOnClickListener(v -> {
num.element++;
});
}
函数中访问的外部环境中的变量,都会用Ref中定义的各种类型的包装类进行包装,对其的访问会进行自动的装箱拆箱工作。与长度为1的数组这种技巧一样,将外部环境中的变量引用的位置由栈移入的堆中。
JavaScript闭包的实现
对于JavaScript,闭包是如何实现的呢?它并不像Java一样把函数中局部变量表放在栈帧中,而是有一个叫作用域链的对象列表,函数调用的时候会创建一个新的对象保存各局部变量,并把它添加到作用域链中。而函数的返回并不一定会让作用域链中的特定对象回收,闭包中的函数同样会持有该作用域链中的对象的引用。所以,可以说JavaScript是真的把闭包中函数外部环境的概念,给作为一个对象存储起来了。
developer.mozilla.org/zh-CN/docs/… www.zhihu.com/question/25… www.iteye.com/blog/rednax…