堆栈可能不够用...考虑一个更简单的情况,其中它们可行
function bar(f) {
alert(f());
}
function foo(x) {
bar(function(){ return x; });
}
foo(42);
在上述情况下,理论上闭包中的
x
可以存在于
foo
的堆栈帧中,因为该闭包不会超过其创建者
foo
的生存期。然而,稍作修改:
function bar(f) {
to_call_later.push(f);
}
闭包将被存储,并且在 foo
已终止并其激活记录的堆栈空间已被回收时可能被调用。显然,x
不能在该堆栈区域中,因为它必须幸存。
因此存在两个问题:
闭包必须有一些存储(环境)。当您考虑两次调用 foo
并传递两个不同的值时,这是显然的,应为 x
创建两个独立的存储空间。如果闭包只是代码,则除非每次调用 foo
时都要生成不同的代码,否则不可能实现此目标。
此存储空间必须至少与闭包本身一样长,而不仅仅是创建闭包的人。
请注意,如果要读取/写入封闭变量,则需要额外的间接层,例如:
function bar(f) {
alert(f());
}
function foo(x) {
var c1 = function() { return ++x; };
var c2 = function() { return x *= 2; };
bar(c1);
bar(c2);
}
foo(42);
换句话说,你可以有几个不同的闭包共享相同的环境。
因此,x不能在foo激活记录的堆栈中,也不能在闭包对象本身中。闭包对象必须有指向x所在位置的指针。
在x86上实现这个的一个可能的解决方案是:
- 使用垃圾回收或引用计数内存管理系统。堆栈远远不足以处理闭包。
- 每个闭包都是一个对象,有两个字段: 指向代码的指针和指向封闭变量(“环境”)的指针数组。
- 在执行代码时,有一个堆栈esp,例如esi指向闭包对象本身(因此(esi)是代码的地址,(esi+4)是第一个封闭变量的地址,(esi+8)是第二个封闭变量的地址,依此类推)。
- 每个变量是一个独立的堆分配对象,只要还有闭包指向它就可以存在。
当然,这是一个非常粗略的方法。例如,SBCL更聪明,未被捕获的变量只在堆栈和/或寄存器中分配。这需要对闭包的使用进行分析。
编辑
假设您只考虑纯函数设置(换句话说,函数/闭包的返回值仅取决于传递的参数,并且闭包状态不能发生变化),那么事情可以简化一些。
您可以将闭包对象中包含捕获的值而不是捕获的变量,并同时使闭包本身成为可复制对象,然后只需要在理论上使用堆栈 (除了有一个问题,即闭包的大小取决于需要捕获多少状态),因此至少对我来说,很难想象在这种情况下使用基于堆栈的合理参数传递和返回值协议。
通过使闭包成为固定大小的对象来消除变量大小问题,您可以看到这个C程序如何只使用堆栈实现闭包(请注意,没有malloc调用)。
#include <stdio.h>
typedef struct TClosure {
int (*code)(struct TClosure *env, int);
int state;
} Closure;
int call(Closure *c, int x) {
return c->code(c, x);
}
int adder_code(Closure *env, int x) {
return env->state + x;
}
int multiplier_code(Closure *env, int x) {
return env->state * x;
}
Closure make_closure(int op, int k) {
Closure c;
c.state = k;
c.code = (op == '+' ? adder_code : multiplier_code);
return c;
}
int main(int argc, const char *argv[]) {
Closure c1 = make_closure('+', 10);
Closure c2 = make_closure('*', 3);
printf("c1(3) = %i, c2(3) = %i\n",
call(&c1, 3), call(&c2, 3));
return 0;
}
Closure
结构体可以进行传递、返回和存储,因为环境是只读的,所以你不会遇到生命周期问题,因为不可变数据可以被复制而不影响语义。
一个 C 编译器可以使用这种方法创建只能按值捕获变量的闭包,这也是 C++11 lambda 提供的功能(你也可以按引用捕获,但程序员需要确保捕获变量的生命周期足够长)。