带有 JavaScript 示例的闭包的工作定义
闭包是一种对象,它包含指向要执行的函数的指针或某种类型的引用以及函数所需数据的实例。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures 的 JavaScript 示例是
function makeAdder(x) {
return function(y) { // create the adder function and return it along with
return x + y; // the captured data needed to generate its return value
};
}
然后可以像这样使用:
var add5 = makeAdder(5); // create an adder function which adds 5 to its argument
console.log(add5(2)); // displays a value of 2 + 5 or 7
用 C 克服的一些障碍
C 编程语言是一种静态类型的语言,与 JavaScript 不同,它也没有垃圾收集和其他一些特性,可以轻松地在 JavaScript 或其他对闭包的内在支持的语言中进行闭包。
标准 C 中闭包的一大障碍是缺乏语言支持 JavaScript 示例中的构造类型,其中闭包不仅包括函数,还包括创建闭包时捕获的数据副本,一种保存状态的方法,然后可以在执行闭包时使用,以及在调用闭包函数时提供的任何其他参数。
但是 C 确实有一些基本的构建块可以提供创建一种闭包的工具。一些困难是(1)内存管理是程序员的职责,没有垃圾收集,(2)函数和数据是分开的,没有类或类类型机制,(3)静态类型,所以没有数据类型的运行时发现或数据大小,以及 (4) 在创建闭包时捕获状态数据的语言设施较差。
使 C 的闭包功能成为可能的一件事是 void * 指针并使用 unsigned char 作为一种通用内存类型,然后通过强制转换将其转换为其他类型。
使用标准 C 的实现,并在此处和那里进行一些扩展
注意: 以下示例依赖于大多数 x86 32 位编译器使用的基于堆栈的参数传递约定。大多数编译器还允许指定调用约定,而不是基于堆栈的参数传递,例如 Visual Studio 的 __fastcall 修饰符。 x64 和 64 位 Visual Studio 的默认设置是默认使用 __fastcall 约定,以便函数参数在寄存器中传递,而不是在堆栈中传递。请参阅 Microsoft MSDN 中的 Overview of x64 Calling Conventions 以及 How to set function arguments in assembly during runtime in a 64bit application on Windows? 以及 How are variable arguments implemented in gcc? 中的各种答案和 cmets。
我们可以做的一件事是解决这个问题,为 C 提供某种闭包工具是为了简化问题。提供对大多数应用程序有用的 80% 解决方案总比没有解决方案要好。
这样一种简化是只支持不返回值的函数,换句话说,函数声明为void func_name()。我们还将放弃函数参数列表的编译时类型检查,因为这种方法在运行时构建函数参数列表。我们放弃的这些东西都不是微不足道的,所以问题是这种 C 语言闭包方法的价值是否超过了我们放弃的东西。
首先让我们定义我们的闭包数据区。闭包数据区表示我们将用于包含闭包所需信息的内存区域。我能想到的最少数据量是指向要执行的函数的指针,以及作为参数提供给函数的数据副本。
在这种情况下,我们将提供函数所需的任何捕获状态数据作为函数的参数。
我们还希望有一些基本的安全防护措施,以便我们能够合理地安全失败。不幸的是,在我们用来实现某种闭包形式的一些变通方法中,安全栏有点弱。
源代码
以下源代码是使用 Visual Studio 2017 社区版在 .c C 源文件中开发的。
数据区是一个结构体,包含一些管理数据、一个指向函数的指针和一个开放式数据区。
typedef struct {
size_t nBytes; // current number of bytes of data
size_t nSize; // maximum size of the data area
void(*pf)(); // pointer to the function to invoke
unsigned char args[1]; // beginning of the data area for function arguments
} ClosureStruct;
接下来我们创建一个初始化闭包数据区的函数。
ClosureStruct * beginClosure(void(*pf)(), int nSize, void *pArea)
{
ClosureStruct *p = pArea;
if (p) {
p->nBytes = 0; // number of bytes of the data area in use
p->nSize = nSize - sizeof(ClosureStruct); // max size of the data area
p->pf = pf; // pointer to the function to invoke
}
return p;
}
此函数旨在接受指向数据区域的指针,这为函数的用户希望如何管理内存提供了灵活性。它们可以使用堆栈上的一些内存或静态内存,也可以通过malloc() 函数使用堆内存。
unsigned char closure_area[512];
ClosureStruct *p = beginClosure (xFunc, 512, closure_area);
或
ClosureStruct *p = beginClosure (xFunc, 512, malloc(512));
// do things with the closure
free (p); // free the malloced memory.
接下来我们提供一个函数,允许我们向闭包添加数据和参数。该函数的目的是建立闭包数据,以便在调用闭包函数时,将为闭包函数提供其工作所需的任何数据。
ClosureStruct * pushDataClosure(ClosureStruct *p, size_t size, ...)
{
if (p && p->nBytes + size < p->nSize) {
va_list jj;
va_start(jj, size); // get the address of the first argument
memcpy(p->args + p->nBytes, jj, size); // copy the specified size to the closure memory area.
p->nBytes += size; // keep up with how many total bytes we have copied
va_end(jj);
}
return p;
}
为了使它更易于使用,让我们提供一个包装宏,它通常很方便,但由于它是 C 处理器文本操作,所以有一些限制。
#define PUSHDATA(cs,d) pushDataClosure((cs),sizeof(d),(d))
所以我们可以使用类似下面的源代码:
unsigned char closurearea[256];
int iValue = 34;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, closurearea), iValue);
dd = PUSHDATA(dd, 68);
execClosure(dd);
调用闭包:execClosure() 函数
最后一部分是execClosure() 函数,它使用其数据执行闭包函数。我们在这个函数中所做的是在调用函数时将闭包数据结构中提供的参数列表复制到堆栈中。
我们所做的是将闭包数据的 args 区域转换为指向包含 unsigned char 数组的结构的指针,然后取消引用该指针,以便 C 编译器在调用之前将参数的副本放入堆栈中闭包中的函数。
为了更轻松地创建 execClosure() 函数,我们将创建一个宏,以便轻松创建所需的各种大小的结构。
// helper macro to reduce type and reduce chance of typing errors.
#define CLOSEURESIZE(p,n) if ((p)->nBytes < (n)) { \
struct {\
unsigned char x[n];\
} *px = (void *)p->args;\
p->pf(*px);\
}
然后我们使用这个宏来创建一系列的测试来确定如何调用闭包函数。此处选择的尺寸可能需要针对特定应用进行调整。这些大小是任意的,并且由于闭包数据很少具有相同的大小,因此不能有效地使用堆栈空间。并且有可能存在比我们允许的更多的关闭数据。
// execute a closure by calling the function through the function pointer
// provided along with the created list of arguments.
ClosureStruct * execClosure(ClosureStruct *p)
{
if (p) {
// the following structs are used to allocate a specified size of
// memory on the stack which is then filled with a copy of the
// function argument list provided in the closure data.
CLOSEURESIZE(p,64)
else CLOSEURESIZE(p, 128)
else CLOSEURESIZE(p, 256)
else CLOSEURESIZE(p, 512)
else CLOSEURESIZE(p, 1024)
else CLOSEURESIZE(p, 1536)
else CLOSEURESIZE(p, 2048)
}
return p;
}
我们返回指向闭包的指针以使其易于使用。
使用开发的库的示例
我们可以如下使用上面的。首先是几个并没有多大作用的示例函数。
int zFunc(int i, int j, int k)
{
printf("zFunc i = %d, j = %d, k = %d\n", i, j, k);
return i + j + k;
}
typedef struct { char xx[24]; } thing1;
int z2func(thing1 a, int i)
{
printf("i = %d, %s\n", i, a.xx);
return 0;
}
接下来我们构建闭包并执行它们。
{
unsigned char closurearea[256];
thing1 xpxp = { "1234567890123" };
thing1 *ypyp = &xpxp;
int iValue = 45;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
free(execClosure(PUSHDATA(dd, iValue)));
dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
dd = PUSHDATA(dd, 68);
execClosure(dd);
dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
dd = PUSHDATA(dd, 145);
dd = PUSHDATA(dd, 185);
execClosure(dd);
}
输出为
i = 45, 1234567890123
i = 68, 1234567890123
zFunc i = 45, j = 145, k = 185
那么柯里化呢?
接下来我们可以对闭包结构进行修改,以允许我们对函数进行柯里化。
typedef struct {
size_t nBytes; // current number of bytes of data
size_t nSize; // maximum size of the data area
size_t nCurry; // last saved nBytes for curry and additional arguments
void(*pf)(); // pointer to the function to invoke
unsigned char args[1]; // beginning of the data area for function arguments
} ClosureStruct;
支持currying和curry point的重置功能
ClosureStruct *curryClosure(ClosureStruct *p)
{
p->nCurry = p->nBytes;
return p;
}
ClosureStruct *resetCurryClosure(ClosureStruct *p)
{
p->nBytes = p->nCurry;
return p;
}
测试的源代码可以是:
{
unsigned char closurearea[256];
thing1 xpxp = { "1234567890123" };
thing1 *ypyp = &xpxp;
int iValue = 45;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
free(execClosure(PUSHDATA(dd, iValue)));
dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
dd = PUSHDATA(dd, 68);
execClosure(dd);
dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
dd = PUSHDATA(dd, 145);
dd = curryClosure(dd);
dd = resetCurryClosure(execClosure(PUSHDATA(dd, 185)));
dd = resetCurryClosure(execClosure(PUSHDATA(dd, 295)));
}
与
的输出
i = 45, 1234567890123
i = 68, 1234567890123
zFunc i = 45, j = 145, k = 185
zFunc i = 45, j = 145, k = 295