函数指针,闭包和Lambda


86

我现在正在学习函数指针,当我阅读有关该主题的K&R章节时,打动我的第一件事是:“嘿,这有点像闭包。” 我知道这个假设在某种程度上根本上是错误的,并且在网上搜索之后,我没有发现对此比较的任何分析。

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

为什么在第二种情况下将函数传递给未命名的函数比在第一种情况下传递的只是普通的日常函数更强大?

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

谢谢。

Answers:


108

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

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

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

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;
}

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

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

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


谢谢,您真的把其他人想知道的想法带回家了。

在编写此代码时,您可能正在使用C的较旧版本,或者不记得要向前声明该函数,但是在测试此功能时,我没有观察到与您提到的行为相同的行为。ideone.com/JsDVBK
smac89 '11

@ smac89-您将lessThan变量设置为全局变量-我明确提到了它作为替代方案。
Mark Brackett

42

作为为具有和没有“真正”闭包的语言编写过编译器的人,我谨不同意上面的一些答案。Lisp,Scheme,ML或Haskell闭包不会动态创建新函数。取而代之的是,它重用了现有功能,但使用了新的自由变量。至少由编程语言理论家将自由变量的集合称为环境

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

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

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

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

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

这种观察导致亨利·贝克(Henry Baker)的明智之举:

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


1
+1来解释和引用OOP实际上是闭包-重用现有函数,但使用新的自由变量-环境的函数(方法)(指向对象实例数据的结构指针,不过是新状态)进行操作。
legends2k 2014年

8

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

用最简单的术语来说,函数指针没有与之关联的范围(除非您计算全局范围),而闭包包括定义它们的方法的范围。使用lambda,您可以编写编写方法的方法。闭包允许您将“某些参数绑定到一个函数,并因此获得一个低等函数”。(摘自Thomas的评论)。您无法在C语言中做到这一点。

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

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

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”。

我希望这是有道理的。


我根据您的评论调整了答案。我仍然对这些条款的细节还不是100%清楚,因此我直接引述了您。:)
Herms,

匿名函数的内联能力是(大多数?)主流编程语言的实现细节-闭包不是必需的。
Mark Brackett

6

闭包=逻辑+环境。

例如,考虑以下C#3方法:

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

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

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


考虑用IEnumerable <Person>替换void
Amy B

1
@David B:干杯,干杯。@edg:我认为这不仅仅是状态,因为它是可变状态。换句话说,如果您执行一个更改局部变量(尽管仍在方法内)的闭包,则该局部变量也会更改。“环境”似乎可以更好地传达给我,但这很笨拙。
乔恩·斯基特

我很欣赏这个答案,但实际上并不能解决任何问题,看起来人只是一个对象,而您只是在上面调用方法。也许只是我不知道C#。

是的,它正在对其上调用方法-但传递的参数是闭包。
乔恩·斯基特

4

在C语言中,函数指针可以作为函数的参数传递,也可以作为函数的值返回,但是函数仅存在于顶层:不能将函数定义相互嵌套。考虑一下C支持嵌套函数可以访问外部函数的变量,同时仍然能够在调用堆栈上下发送函数指针的情况。(要遵循此说明,您应该了解如何使用C和大多数类似语言实现函数调用的基础:浏览Wikipedia上的调用堆栈条目。)

嵌套函数的指针是哪种对象?它不能只是代码的地址,因为如果调用它,它将如何访问外部函数的变量?(请记住,由于递归,一次可能有多个不同的外部函数处于活动状态。)这被称为funarg问题,并且有两个子问题:向下funargs问题和向上funargs问题。

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

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

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


3

Lambda是一个动态定义的匿名函数。对于C闭包(或二者的说服力),您只是无法在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]> 

2

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

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

我不愿意将lambda与闭包等同,因为我不确定所有语言中的lambda都是闭包,有时我认为lambda只是局部定义的匿名函数,没有绑定变量(Python 2.1之前的版本?)。


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。


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


1

主要区别来自C语言中缺少词法作用域。

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

闭包OTOH以“外部变量”或“ upvalues”的形式具有其自身的状态。使用词法作用域,它们可以随意或共享。您可以使用相同的功能代码创建许多闭包,但使用不同的变量实例。

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

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


2
WTF?C绝对具有词法作用域。
路易斯·奥利维拉

1
它具有“静态作用域”。据我了解,词法作用域是一种更复杂的功能,用于在具有动态创建函数的语言上维护相似的语义,这些函数随后称为闭包。
哈维尔

1

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

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

闭包是定义$count变量的环境。它仅对increment子例程可用,并且在两次调用之间保持不变。


0

在C语言中,函数指针是在取消引用时将调用函数的指针,闭包是一个包含函数的逻辑和环境(变量及其绑定的值)的值,而lambda通常指的是实际上是一个未命名的函数。在C中,函数不是一流的值,因此不能传递给它,因此您必须传递一个指向它的指针,但是在函数式语言(如Scheme)中,您可以传递函数的方式与传递任何其他值的方式相同

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.