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

那么为什么C风格的函数指针与闭包或lambdas根本不同呢?据我所知,它与函数指针仍指向已定义(命名)函数的事实有关,这与匿名定义函数的做法相反。

为什么将一个函数传递给一个在第二种情况下看起来更强大的函数,它是未命名的,而不是第一种只传递正常日常函数的函数?

请告诉我如何以及为什么我错误地比较两者。

感谢。

有帮助吗?

解决方案

lambda(或结束)封装了函数指针和变量。这就是为什么在C#中你可以这样做:

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

我在那里使用了一个匿名委托作为一个闭包(它的语法更清晰,更接近C而不是lambda等价物),它将lessThan(一个堆栈变量)捕获到闭包中。在计算闭包时,将继续引用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不在范围内,我要么必须通过将其传递给链中的每个函数来手动保持它,或者将其提升为全局函数。

虽然支持闭包的大多数主流语言都使用匿名函数,但并不要求这样做。你可以拥有没有匿名函数的闭包,以及没有闭包的匿名函数。

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

其他提示

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

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

你可以在C中模拟所有这些,但这是一个痛苦的屁股。两种技术很受欢迎:

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

  2. 将指针传递给struct,其中struct包含自由变量的值以及指向代码的指针。

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

    这一观察导致了亨利贝克的睿智:

      

    Algol / Fortran世界的人们多年来一直抱怨说,他们不了解在未来的有效编程中可能使用的功能关闭。然后发生了“面向对象编程”的革命,现在每个人都使用函数闭包编程,除了他们仍然拒绝称之为。

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

用最简单的术语来说,函数指针没有与它们相关的范围(除非你计算全局范围),而闭包包括定义它们的方法的范围。使用lambdas,您可以编写一个编写方法的方法。闭包允许您将“某些参数绑定到函数并获得较低的arity函数”。 (取自托马斯的评论)。你不能用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',即使它们不在全局范围内。

我希望这是有道理的。

Closure =逻辑+环境。

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

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

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

有关详情,请查看我的有关关闭的文章通过C#1,2和3,展示了闭包如何让事情变得更容易。

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

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

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

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

Lambdas(匿名函数)实际上是一个单独的问题,但通常是一种语言你动态定义匿名函数也可以让你把它们作为函数值返回,所以它们最终会成为闭包。

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]> 

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

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

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

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

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

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 以相同的方式调用它们。

主要区别在于C语言缺乏词汇范围。

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

闭包,OTOH,以“外部变量”或“upvalues”的形式有自己的状态。使用词法范围可以将它们设为私有或共享。您可以使用相同的功能代码创建大量闭包,但不同的变量实例。

一些闭包可以共享一些变量,因此可以是对象的接口(在OOP意义上)。要在C中创建它,你必须将一个结构与一个函数指针表相关联(这就是C ++所做的,使用类vtable)。

总之,一个闭包是一个函数指针PLUS的一些状态。它是一个更高层次的构造

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top