87

我刚刚开始学习函数指针,当我阅读有关该主题的 K&R 章节时,我首先想到的是,“嘿,这有点像一个闭包。” 我知道这个假设在某种程度上是根本错误的,在网上搜索后,我并没有发现任何对这种比较的分析。

那么为什么 C 风格的函数指针与闭包或 lambda 完全不同呢?据我所知,这与函数指针仍然指向已定义(命名)函数的事实有关,而不是匿名定义函数的做法。

为什么将函数传递给在第二种情况下被视为更强大的函数,在这种情况下,它没有命名,而不是在第一种情况下,它只是一个普通的,正在传递的日常函数?

请告诉我如何以及为什么将两者如此密切地比较是错误的。

谢谢。

4

12 回答 12

109

lambda(或闭包)封装了函数指针和变量。这就是为什么在 C# 中,您可以执行以下操作:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

我在那里使用了一个匿名委托作为闭包(它的语法比等效的 lambda 更清晰,更接近 C),它将 lessThan (堆栈变量)捕获到闭包中。评估闭包时,将继续引用 lessThan(其堆栈帧可能已被破坏)。如果我改变小于,那么我改变比较:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

在 C 中,这将是非法的:

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

虽然我可以定义一个带有 2 个参数的函数指针:

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

但是,现在我在评估它时必须传递 2 个参数。如果我希望将此函数指针传递给 lessThan 不在范围内的另一个函数,我将不得不通过将其传递给链中的每个函数或将其提升为全局函数来手动使其保持活动状态。

尽管大多数支持闭包的主流语言都使用匿名函数,但并没有要求。你可以有没有匿名函数的闭包,也可以有没有闭包的匿名函数。

总结:闭包是函数指针+捕获变量的组合。

于 2008-10-16T15:07:11.707 回答
44

作为一个为有和没有“真正”闭包的语言编写编译器的人,我恭敬地不同意上面的一些答案。Lisp、Scheme、ML 或 Haskell 闭包不会动态创建新函数。相反,它重用了现有函数,但使用了新的自由变量。自由变量的集合通常被称为environment,至少被编程语言理论家称为。

闭包只是包含函数和环境的聚合。在新泽西编译器的标准 ML 中,我们将一个表示为一条记录;一个字段包含指向代码的指针,其他字段包含自由变量的值。编译器通过分配一个新记录来动态创建一个新的闭包(不是函数),该记录包含指向相同代码的指针,但自由变量具有不同的值。

您可以在 C 中模拟所有这些,但这很麻烦。两种技术很流行:

  1. 传递一个指向函数(代码)的指针和一个指向自由变量的单独指针,以便将闭包拆分为两个 C 变量。

  2. 传递一个指向结构的指针,其中结构包含自由变量的值以及指向代码的指针。

当您尝试在 C 中模拟某种多态性并且您不想透露环境的类型时,技术#1 是理想的——您使用 void* 指针来表示环境。例如,查看 Dave Hanson 的C Interfaces and Implementations。技术 #2 更类似于函数式语言的本机代码编译器中发生的情况,也类似于另一种熟悉的技术……具有虚拟成员函数的 C++ 对象。实现几乎相同。

这一观察引出了亨利贝克的一句俏皮话:

多年来,Algol/Fortran 世界的人们抱怨说,他们不明白函数闭包在未来的高效编程中可能有什么用途。然后发生了“面向对象编程”革命,现在每个人都使用函数闭包进行编程,只是他们仍然拒绝这样称呼它们。

于 2008-12-06T04:04:36.560 回答
8

在 C 中,你不能定义内联函数,所以你不能真正创建一个闭包。您所做的只是传递对某些预定义方法的引用。在支持匿名方法/闭包的语言中,方法的定义要灵活得多。

用最简单的术语来说,函数指针没有与之关联的范围(除非您计算全局范围),而闭包包括定义它们的方法的范围。使用 lambdas,您可以编写一个编写方法的方法。闭包允许你将“一些参数绑定到一个函数,从而得到一个低参数的函数”。(取自托马斯的评论)。你不能在 C 中做到这一点。

编辑:添加一个示例(我将使用 Actionscript-ish 语法,因为这就是我现在的想法):

假设您有一些方法将另一个方法作为其参数,但没有提供在调用该方法时将任何参数传递给该方法的方法?比如说,某些方法在运行您传递的方法之前会导致延迟(愚蠢的例子,但我想保持简单)。

function runLater(f:Function):Void {
  sleep(100);
  f();
}

现在假设您希望用户 runLater() 延迟对象的某些处理:

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

您传递给 process() 的函数不再是静态定义的函数。它是动态生成的,并且能够包含对定义方法时范围内的变量的引用。因此,它可以访问“o”和“objectProcessor”,即使它们不在全局范围内。

我希望这是有道理的。

于 2008-10-16T14:47:09.813 回答
6

闭包 = 逻辑 + 环境。

例如,考虑这个 C# 3 方法:

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

lambda 表达式不仅封装了逻辑(“比较名称”),还封装了环境,包括参数(即局部变量)“名称”。

有关这方面的更多信息,请查看我关于闭包的文章,该文章将带您了解 C# 1、2 和 3,展示了闭包如何使事情变得更容易。

于 2008-10-16T14:49:17.110 回答
4

在 C 中,函数指针可以作为参数传递给函数,也可以作为函数的值返回,但函数只存在于顶层:不能将函数定义相互嵌套。想想 C 需要什么来支持可以访问外部函数变量的嵌套函数,同时仍然能够在调用堆栈上下发送函数指针。(要遵循这个解释,您应该了解如何在 C 和大多数类似语言中实现函数调用的基础知识:浏览 Wikipedia 上的调用堆栈条目。)

什么样的对象是指向嵌套函数的指针?它不能只是代码的地址,因为如果你调用它,它如何访问外部函数的变量?(请记住,由于递归,一次可能有多个不同的外部函数调用。)这称为函数参数问题,有两个子问题:向下函数参数问题和向上函数参数问题。

向下的函数参数问题,即,将函数指针“向下”作为参数发送给您调用的函数,实际上与 C 并不兼容,并且 GCC支持将嵌套函数作为向下函数参数。在 GCC 中,当你创建一个指向嵌套函数的指针时,你真的得到了一个指向trampoline的指针,这是一段动态构造的代码,它设置静态链接指针,然后调用真正的函数,它使用静态链接指针来访问外部函数的变量。

向上的 funargs 问题更加困难。GCC 不会阻止您在外部函数不再活动后让蹦床指针存在(在调用堆栈上没有记录),然后静态链接指针可能指向垃圾。不能再在堆栈上分配激活记录。通常的解决方案是在堆上分配它们,让代表嵌套函数的函数对象只指向外部函数的激活记录。这样的对象称为闭包。然后,该语言通常必须支持垃圾回收,以便一旦没有更多指针指向它们,就可以释放记录。

Lambdas(匿名函数)确实是一个单独的问题,但通常一种允许您动态定义匿名函数的语言也允许您将它们作为函数值返回,因此它们最终成为闭包。

于 2008-10-16T15:41:56.753 回答
3

lambda 是一个匿名的、动态定义的函数。你不能在 C 中做到这一点......至于闭包(或两者的结合),典型的 lisp 示例看起来类似于:

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

在 C 语言中,您可以说 的词法环境(堆栈)get-counter被匿名函数捕获,并在内部进行修改,如下例所示:

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]> 
于 2008-10-16T15:01:37.130 回答
2

闭包意味着从函数定义的角度来看一些变量与函数逻辑绑定在一起,比如能够动态声明一个迷你对象。

C 和闭包的一个重要问题是分配在堆栈上的变量将在离开当前范围时被销毁,无论闭包是否指向它们。这将导致人们在粗心地返回指向局部变量的指针时遇到的那种错误。闭包基本上意味着所有相关变量都是堆上的引用计数或垃圾收集项。

我不习惯将 lambda 等同于闭包,因为我不确定所有语言中的 lambda 都是闭包,有时我认为 lambda 只是本地定义的匿名函数,没有变量绑定(Python pre 2.1?)。

于 2009-01-13T12:40:12.157 回答
2

在 GCC 中,可以使用以下宏来模拟 lambda 函数:

#define lambda(l_ret_type, l_arguments, l_body)       \
({                                                    \
    l_ret_type l_anonymous_functions_name l_arguments \
    l_body                                            \
    &l_anonymous_functions_name;                      \
})

来源示例:

qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
     lambda (int, (const void *a, const void *b),
             {
               dump ();
               printf ("Comparison %d: %d and %d\n",
                       ++ comparison, *(const int *) a, *(const int *) b);
               return *(const int *) a - *(const int *) b;
             }));

使用这种技术当然会消除您的应用程序与其他编译器一起工作的可能性,并且显然是“未定义”的行为,所以 YMMV。

于 2015-11-24T06:28:49.973 回答
2

闭包捕获环境中的自由变量。即使周围的代码可能不再处于活动状态,环境仍将存在。

Common Lisp 中的一个示例,其中MAKE-ADDER返回一个新的闭包。

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

使用上述功能:

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

请注意,该DESCRIBE函数显示两个闭包的函数对象相同,但环境不同。

Common Lisp 使闭包和纯函数对象(那些没有环境的对象)都成为函数,并且可以以相同的方式调用两者,这里使用FUNCALL.

于 2015-11-26T19:08:03.663 回答
1

主要区别在于 C 中缺少词法作用域。

函数指针就是这样,一个指向代码块的指针。它引用的任何非堆栈变量都是全局的、静态的或类似的。

闭包 OTOH 以“外部变量”或“上值”的形式拥有自己的状态。它们可以是私有的或共享的,使用词法范围。您可以使用相同的函数代码创建许多闭包,但变量实例不同。

一些闭包可以共享一些变量,对象的接口也可以共享(在 OOP 意义上)。要在 C 中实现这一点,您必须将结构与函数指针表相关联(这就是 C++ 所做的,具有类 vtable)。

简而言之,闭包是一个函数指针加上一些状态。这是一个更高级别的构造

于 2008-10-16T14:54:37.270 回答
1

大多数响应表明闭包需要函数指针,可能指向匿名函数,但正如Mark 所写,闭包可以与命名函数一起存在。这是 Perl 中的一个示例:

{
    my $count;
    sub increment { return $count++ }
}

闭包是定义$count变量的环境。它仅可用于increment子例程并在调用之间持续存在。

于 2008-10-17T14:29:56.697 回答
0

在 C 中,函数指针是在取消引用函数时将调用函数的指针,闭包是包含函数逻辑和环境(变量及其绑定到的值)的值,而 lambda 通常指的是一个值实际上是一个未命名的函数。在 C 中,函数不是第一类值,所以它不能被传递,所以你必须传递一个指向它的指针,但是在函数式语言(如 Scheme)中,你可以像传递任何其他值一样传递函数

于 2008-10-16T15:07:26.247 回答