一个朋友曾经问过我这样一个问题:
void register_event_handler(void *f_ctx, void (*f)(void *ctx));我对
f_ctx的作用有点困惑。在这里,f是一个处理函数,当事件被触发时被调用,而f_ctx是--根据文档--一些指针参数,每当它被调用时被传递给f。为什么我们需要f_ctx?光是f不就够了吗?
这是像 C 这样的低级语言的技巧,在这些语言中,函数是用一个原始的函数指针来表示的,它并不存储一个包围的环境(有时称为上下文)。在高级语言中,它不需要支持 第一类函数的支持,例如 Python,因为这些语言允许将函数嵌套在其他函数中,并且会自动将包围环境存储在函数对象中,这种组合称为闭包。
当你想写一个依赖于在编译时不知道的外部参数的函数时,就需要一个环境指针f_ctx 。f_ctx 参数允许你以你喜欢的方式将这些外部参数偷渡到f 。
用一个例子来说明这一点可能是最好的,考虑一个像这样的一维数值积分器:
double integrate_1(
double (*f)(double x), /* function to be integrated */
double x1,
double x2
);
如果你提前知道函数的完整形式f ,这就很好用。但如果不是这样呢--如果该函数需要参数呢?比如我们想用积分来计算伽马函数:
double integrand(double x)
{
double t = /* where do we get "t" from?? */;
return pow(x, t - 1.0) * exp(-x);
}
double gamma_function(double t)
{
/* how do we send the value of "t" into "integrand"? */
return integrate_1(&integrand, 0.0, INFINITY) / M_PI;
}
使用integrand_1 ,只有三种方法可以做到这一点:
-
将
t存储到一个全局变量中,牺牲了线程安全。从不同的线程同时调用gamma_function,这是很糟糕的,因为它们都会试图使用同一个全局变量。 -
编写原始的机器代码,在运行中创建一个积分。这可以用线程安全和可重入的方式来实现,但它既没有效率,也不便于携带,还抑制了编译器的优化。
然而,如果数值积分器要像这样重新设计:
double integrate_2(
double (*f)(void *f_ctx, double x),
void *f_ctx, /* passed into every invocation of "f" */
double x1,
double x2
);
那么有一个更简单的解决方案可以避免所有这些问题:
double integrand(void *ctx, double x)
{
double t = *(double *)ctx;
return pow(x, t - 1.0) * exp(-x);
}
double gamma_function(double t)
{
return integrate_2(&integrand, &t, 0.0, INFINITY) / M_PI;
}
这是线程安全的、可重入的、高效的和可移植的。
如前所述,这个问题在Python这样的语言中并不存在,在这种语言中,函数可以嵌套在其他函数中(或者说,它被语言本身自动处理了):
def gamma_function(t):
def integrand(x):
return x ** (t - 1.0) * math.exp(-x)
return integrate(integrand, 0.0, math.inf) / math.pi