闭包是现在很多编程语言都支持的一项功能强大的特性,但对于很多人来说,闭包这个东西确实有点晦涩,不太好理解,我觉得主要可能有以下几个原因:
名字没取好说实话,我觉得不管是英文 “closure” 还是中文“闭包”,名字都不好,当然主要是英文名字没取好,中文只是按字面意思翻译罢了。那有没有更好的名字?我觉得可以参考高阶函数(Higher-order function)的叫法,就叫有状态函数(Stateful function)。不了解没有闭包时的写法闭包出现的历史好像很早了,但主要是一些函数式语言,像 c/c++ 这类早期被广泛使用的工业语言,是不支持闭包的,但闭包使用的一些场景,c/c++ 里面也会遇到并且也能解决,只是实现 不同,跟用闭包来实现相比更不方便而已。了解 c/c++ 里面类似场景的写法,可以帮助理解为什么要有闭包,从而更好的理解闭包。下面我们就尝试一下,先分析不支持闭包的一些语言的写法,然后再对比支持闭包的语言的写法,把闭包的来龙去脉分析清楚,从而彻底搞懂闭包。
c 回调函数
回调函数(callback)是 c 语言里常用的一种编程手法,特别是在实现一些异步功能或者设计库 API 时,更是必不可少。我们来看看 c 里面一般如何做的(假设实现一个事件的异步处理功能)。
没学过 c 语言也关系,代码很简单,应该能大致看得懂。
首先定义回调函数的类型 callback_t, 即类似 " void callback (int event) {...} " 的形式,定义用到了函数指针的定义
typedef void (*callback_t)(int event);
接下来定义事件注册接口 register_callback
void register_callback(int event, callback_t callback);
然后使用这个接口注册事件 EVENT_RECV (比如是TCP的数据接收事件)的处理函数为 recv
#define EVENT_RECV 1 void recv(int event) { printf("event: %d\n", event); } register_callback(EVENT_RECV, recv);
上面的实现有个问题,就是在回调函数里无法访问调用者的数据(比如当前状态等),这样在实际使用中,显然是不可行的。
接下来我们改造一下。
首先修改回调函数的类型 callback_t, 增加一个参数 user_data,类型为通用指针(可以传任何数据)。
typedef void (*callback_t)(int event, void *user_data);
事件注册接口 register_callback,也同样增加一个参数 user_data。
void register_callback(int event, callback_t callback, void *user_data);
然后注册事件回调时,传入 user_data。
#define EVENT_RECV 1 user_t *user = create_user(...); // 此处表示创建一个用户, 代码只是示意 void recv(int event, void *user_data) { user_t *user = (user_t*)user_data; // 把通用指针转为user_t类型 printf("event: %d, from user: %p\n", user); } register_callback(EVENT_RECV, recv, user);
以上就是 c 语言里使用回调的正常做法,通过在回调函数和注册回调接口都增加一个通用指针类型参数,把外部数据传给回调,从而使回调函数内部可以访问外部数据,这样做可以正常工作,但有几个问题:
回调函数和注册回调接口需要增加参数 user_data,且要显式的传递 user_data。为了通用性, user_data 只能使用通用指针,使用不方便且编译器无法做类型检查。c++ 函数对象
注意:此处的函数对象不是函数,因为跟 Javascript、Python 等语言的一切皆对象不一样,c++ 的函数不是一等公民( first-class function ), 也就是函数不是对象,不能有成员变量。
c++ 是兼容 c 的,所以以上 c 的做法在 c++ 上也能实现。但 c++ 增加了面向对象的支持,因此可以有一些不一样的写法。
首先先看一下c++面向对象的常规用法
class A { void handle(int event) { printf("event: %d\n", event); } } A a; a.handle(event);
c++ 支持运算符重载,因此可以有下面写法
class A { void operator()(int event) // 重载括号运算符 { printf("event: %d\n", event); } } A a; a(event); // 像调用函数一样调用对象
通过重载对象的括号运算符 (),从而让对象可以像函数一样直接调用,这样的对象叫做函数对象( functor )。函数对象除了可以像函数一样直接调用外,还有一个更大的好处,就是函数对象可以有成员变量,因此可以保存数据,如果用来替代c的回调将会非常方便。
下面我们就来尝试修改一下。
class Functor { user_t user_data; // 成员变量,可以保存数据,比如状态 Functor(data) // 构造函数 { user_data = data; } void operator()(int event) // 重载括号运算符 { printf("event: %d, from user: %p\n", user_data); // 此处可以直接访问user_data,只是示意而已 } }
事件注册接口 register_callback,回调 callback 的类型变为函数对象
void register_callback(int event, Functor callback);
使用函数对象来重新实现
#define EVENT_RECV 1 callback = new Functor(data); // 构造函数对象 register_callback(EVENT_RECV, callback);
可以看到,通过使用函数对象,可以避免c回调写法的问题,确实是个很大的提升。实际上函数对象在 c++ 的标准库中被大量的使用,但是c++的这种写法也不是就是完美的。
首先需要定义函数对象,然后每次注册回调时,都需要生成函数对象,对于有些人来说,这些步骤还是挺麻烦的,要是能把这些都省了,那就真的完美了。
闭包
闭包的写法就是把 c++ 的函数对象的定义和创建都省了,完全由编译器或解释器自动化了(当然,实际上编译器或解释器内部不一定用函数对象来实现,比如 Javascript 函数本身就是对象,只要把外部变量保存到成员变量就可以了)。
我们看一下 Javascript 闭包的写法。
function closureTest(){ var user_data = getXXX....(...) function recv(event){ console.log('user_data: ', user_data); } register_callback(EVENT_RECV, recv); }
内部函数 recv 访问了外部变量 user_data,因此变成了一个闭包,可以认为解释器会把 recv 函数从一个普通函数变成一个类似c++函数对象,这个对象有一个成员变量user_data,但是这些都是解释器自动完成的,因此非常方便。
Javascript 生成闭包还可以使用下面 。
function makeClosure(){ var user_data = getXXX....(...) return function (event){ // 返回一个内部函数 console.log('user_data: ', user_data); } } recv = makeClosure() register_callback(EVENT_RECV, recv);
makeClosure 返回了一个内部函数,由于这个内部函数访问了它的外部变量 user_data, 因此变成一个闭包。
其他语言如 Python 对闭包的用法,基本和 Javascript 类似。