代码之家  ›  专栏  ›  技术社区  ›  Garnet Ulrich

函数指针、闭包和lambda

  •  81
  • Garnet Ulrich  · 技术社区  · 16 年前

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

    那么,为什么C样式的函数指针与闭包或lambda有本质的区别呢?据我所知,这与函数指针仍然指向定义的(命名的)函数这一事实有关,而不是匿名定义函数的做法。

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

    请告诉我如何和为什么我错的比较这两个如此密切。

    谢谢。

    12 回复  |  直到 7 年前
        1
  •  106
  •   Mark Brackett    16 年前

    λ(或) closure )封装函数指针和变量。这就是为什么在C中,您可以做到:

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

    我在那里使用了一个匿名委托作为一个闭包(它的语法比lambda等价物更清晰、更接近C),它将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
    }
    

    尽管我可以定义一个接受两个参数的函数指针:

    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不在范围内的另一个函数,那么我要么将其传递给链中的每个函数,要么将其提升为全局函数,从而手动使其保持活动状态。

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

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

        2
  •  40
  •   Norman Ramsey    16 年前

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

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

    你可以在C语言中模拟所有这些,但这是一种屁股痛。有两种技术很流行:

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

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

    当你试图模拟某种 多态性 在C语言中,您不想显示环境的类型——您使用一个void*指针来表示环境。举个例子,看看戴夫·汉森的 C Interfaces and Implementations . 技术2类似于函数语言的本地代码编译器,也类似于另一种熟悉的技术…具有虚拟成员函数的C++对象。实现几乎是相同的。

    这一观察引起了亨利·贝克的明智之举:

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

        3
  •  9
  •   Pacerier    10 年前

    在C语言中,不能直接定义函数,所以不能真正创建一个闭包。您所要做的就是传递对某个预定义方法的引用。在支持匿名方法/闭包的语言中,方法的定义更加灵活。

    在最简单的术语中,函数指针没有与它们相关联的作用域(除非计算全局作用域),而闭包则包括定义它们的方法的作用域。使用lambda,可以编写编写方法的方法。闭包允许您将“一些参数绑定到一个函数,结果得到一个较低的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”,即使它们不在全局范围内。

    我希望这是有道理的。

        4
  •  6
  •   Jon Skeet    16 年前

    关闭=逻辑+环境。

    例如,考虑这个c 3方法:

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

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

    有关更多信息,请查看我的 article on closures 这将带您完成C 1、2和3,展示闭包如何使事情变得更简单。

        5
  •  4
  •   Jouni K. Seppänen    16 年前

    在C语言中,函数指针可以作为参数传递给函数,也可以作为值从函数返回,但函数只存在于顶层:不能彼此嵌套函数定义。考虑一下,对于C来说,支持可以访问外部函数变量的嵌套函数需要什么,同时仍然能够向上和向下发送函数指针。(要遵循此说明,您应该了解如何使用C语言和大多数类似语言实现函数调用的基本知识:浏览 call stack 维基百科上的条目。)

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

    向下函数问题,即将函数指针“向下堆栈”作为参数发送到您调用的函数,实际上与c和gcc不兼容。 supports 作为向下函数的嵌套函数。在gcc中,当创建指向嵌套函数的指针时,实际上会得到指向 trampoline ,一段动态构造的代码,用于设置 静态链接指针 然后调用real函数,该函数使用静态链接指针访问外部函数的变量。

    向上函数问题更为困难。gcc不会阻止您在外部函数不再处于活动状态(调用堆栈上没有记录)之后让Trampoline指针存在,然后静态链接指针可能指向垃圾。不能再在堆栈上分配激活记录。通常的解决方案是在堆上分配它们,让表示嵌套函数的函数对象只指向外部函数的激活记录。这样的对象称为 closure . 语言通常必须支持 garbage collection 以便在不再有指向记录的指针时释放这些记录。

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

        6
  •  3
  •   dsm    16 年前

    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]> 
    
        7
  •  2
  •   Andy Dent    16 年前

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

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

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

        8
  •  2
  •   secretformula    9 年前

    在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;                      \
    })
    

    例子来自 source :

    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。

        9
  •  2
  •   Rainer Joswig mmmmmm    9 年前

    这个 关闭 捕捉到 自由变量 在一个 环境 . 即使周围的代码不再处于活动状态,环境仍然存在。

    公共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 函数显示 函数对象 对于两者 闭包 是一样的,但是 环境 是不同的。

    公共lisp使得闭包和纯函数对象(那些没有环境的对象)都是 功能 一个人可以用同样的方式调用两个,这里使用 FUNCALL .

        10
  •  1
  •   Javier    16 年前

    主要区别在于汉语词汇范围的缺失。

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

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

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

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

        11
  •  1
  •   Community CDub    8 年前

    大多数响应都表明闭包需要函数指针,可能是指向匿名函数,但作为 Mark wrote 闭包可以与命名函数一起存在。下面是Perl中的一个示例:

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

    闭包是定义 $count 变量。它只对 increment 子例程,并在调用之间保持。

        12
  •  0
  •   HasaniH    16 年前

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