Closure function 说白了就是嵌套函数可以访问函数定义是当时的 scope 的变量。
先看下面一段话:
Nested function is not supported by C because we cannot define a function within another function in C. We can declare a function inside a function, but it’s not a nested function. Because nested functions definitions can not access local variables of the surrounding blocks, they can access only global variables of the containing module. This is done so that lookup of global variables doesn’t have to go through the directory. As in C, there are two nested scopes: local and global (and beyond this, built-ins). Therefore, nested functions have only a limited use. If we try to approach nested function in C, then we will get compile time error.
大概意思是说,C 中不可以在函数内部定义函数,就算我们在函数内声明了一个函数,但是那也不算是嵌套函数。说到这里你大概会想到的实现方式会是这个样子:
#include <stdio.h>
typedef void (*func_t)();
void func_test(int i) {
printf("%d\n", i);
}
func_t closure(int i) {
return func_test;
}
int main(int argc, char const *argv[]) {
func_t cb = closure(1);
cb(1);
cb(1);
return 0;
}
这样在函数内声明的方式,其实是没有权限访问当前定义域内的变量的,你可以按照你的想法这个基础上随便怎么修改,都无济于事。
方案一
用 C 的 struct 来实现,C 的 struct 内部是可以定义函数的,然后我们在函数中传入当前的 context,其实也算是曲折的方式实现了 closure 的特性。看代码会一目了然:
#include <stdio.h>
#include <stdlib.h>
struct func_struct {
int x;
void (*call)(struct func_struct *);
};
void func_call(struct func_struct *f) {
f->x++;
printf("%d\n", f->x);
}
struct func_struct *closure(int x) {
struct func_struct *func = (struct func_struct *)malloc(sizeof(struct func_struct));
func->x = x;
func->call = func_call;
return func;
}
int main(int argc, char const *argv[]) {
struct func_struct *f = closure(1);
f->call(f);
f->call(f);
f->call(f);
free(f);
return 0;
}
代码的输出结果也是如预期一样,是 234 依次输出。为了用 closure 的特性,我们需要把闭包中所有的变量封装到 struct 中,其实用起来并不是那么舒服。
说点题外话,很多人说 Go,Rust,C 有点像,其实在 struct method 这个地方时很像的。看下面三段代码:
#include <stdio.h>
#include <stdlib.h>
struct test {
void (*method)();
};
void method() {
printf("call method\n");
}
int main(int argc, char const *argv[]) {
struct test *t = (struct test *)malloc(sizeof(struct test));
t->method = method;
t->method();
return 0;
}
package main
type test struct{}
func (t test) method() {
println("call method")
}
func main() {
var t test
t.method()
}
struct Test {}
impl Test {
pub fn method() {
println!("call method");
}
}
fn main() {
Test::method();
}
Go 和 Rust 实现起来在代码行数上都很少,很简洁,很现代化。
方案二
这个方案里边用到的不是 C 本身的特性了,我们用编译器 Clang 本身的特性,Clang 是 LLVM 的编译前端,LLVM 中有这么一个 block 的特性,那么 Clang 当然也可以用这个特性,看这里,有一些介绍。Talk is cheap,show me the code:
#include <Block.h>
#include <stdio.h>
typedef int (^IntBlock)();
IntBlock MakeCounter(int start, int increment) {
__block int i = start;
return Block_copy(^{
int ret = i;
i += increment;
return ret;
});
}
int main(void) {
IntBlock mycounter = MakeCounter(5, 2);
printf("First call: %d\n", mycounter());
printf("Second call: %d\n", mycounter());
printf("Third call: %d\n", mycounter());
/* 由于是复制的块,因此需要释放 */
Block_release(mycounter);
return 0;
}
这段代码用 GCC 是编译不过去的,需要用 Clang 来编译。需要加上额外的参数 clang -fblocks test.c,将会生成 a.out。
方案三
GCC 里边有没有相对应的特性呢?答案是有的。
#include <stdio.h>
int foo(int a) {
int square(int z) { return z * z; }
return square(a);
}
int main(void) {
printf("%d\n", foo(2));
return 0;
}
这段代码需要这么编译 gcc -std=c11 test.c,Clang 是编译不通过的,用到的是 GCC 单独为此做的扩展,详情看这里。那么我们最开始的那种方式来实现 closure,也是 OK,只不过要把函数定义在函数内部就 OK 了。
总结
方案中后两种都是利用编译器的特性来做的,有点不是特别好,但是编译器既然有这个特性我们为何不可以用呢?存在即合理。个人觉得 GCC 的方式侵入性很小用起来很舒服。第一种方案写起来没有什么灵活性,太过于繁琐啰嗦,但是是最稳的方式,这么写没人可以指责你。