什么是封包?


155

我时不时地看到“关闭”被提及,我试图查找它,但是Wiki没有给出我理解的解释。有人可以帮我吗?


如果您知道Java / C#,希望此链接对您有所帮助-http ://www.developerfusion.com/article/8251/the-beauty-of-closures/
Gulshan

1
关闭很难理解。您应该尝试单击该Wikipedia文章的第一句中的所有链接,并首先理解这些文章。
Zach


3
但是,闭包和类之间的根本区别是什么?好的,只有一个公共方法的类。
biziclop 2011年

5
@biziclop:您可以使用类模拟闭包(Java开发人员必须这样做)。但是创建它们通常会比较冗长一些,并且您不必手动管理自己正在处理的内容。(顽固的lispers提出了类似的问题,但很可能得出另一个结论-当您有闭包时,不需要语言级别的OO支持)。

Answers:


141

(免责声明:这是一个基本的解释;就定义而言,我正在简化一下)

想到闭包的最简单方法是一个可以存储为变量函数(称为“第一类函数”),该函数具有特殊的功能,可以访问在其创建的作用域内本地的其他变量。

示例(JavaScript):

var setKeyPress = function(callback) {
    document.onkeypress = callback;
};

var initialize = function() {
    var black = false;

    document.onclick = function() {
        black = !black;
        document.body.style.backgroundColor = black ? "#000000" : "transparent";
    }

    var displayValOfBlack = function() {
        alert(black);
    }

    setKeyPress(displayValOfBlack);
};

initialize();

分配给和的功能1是闭包。您可以看到它们都引用了boolean变量,但是该变量是在函数外部分配的。因为对于定义函数的作用域局部的,所以保留了指向该变量的指针。document.onclickdisplayValOfBlackblackblack

如果将其放在HTML页面中:

  1. 单击更改为黑色
  2. 点击[输入]以查看“真”
  3. 再次单击,变回白色
  4. 点击[输入]以查看“假”

这说明两者都可以访问相同的 black,并且可以用于存储状态而无需任何包装对象。

调用此命令setKeyPress是为了演示如何像任何变量一样传递函数。闭包中保留的范围仍然是定义函数的范围

闭包通常用作事件处理程序,尤其是在JavaScript和ActionScript中。正确使用闭包将帮助您将变量隐式绑定到事件处理程序,而无需创建对象包装。但是,粗心的使用会导致内存泄漏(例如,当未使用但保留的事件处理程序是唯一保留在内存中的大对象(尤其是DOM对象)上以防止垃圾收集时,)。


1:实际上,JavaScript中的所有函数都是闭包。


3
在阅读您的答案时,我的脑海里闪过一个灯泡。非常感激!:)
Jay

1
既然black在函数内部声明了,在栈展开时不会被破坏吗?
gablin 2011年

1
@gablin,这就是具有闭包的语言的独特之处。具有垃圾回收的所有语言的工作方式几乎相同-当不再保留对对象的引用时,可以将其销毁。每当在JS中创建函数时,本地作用域都会绑定到该函数,直到该函数被销毁为止。
妮可(Nicole)

2
@gablin,这是一个好问题。我认为他们不能— 但是我只提起了垃圾回收,因为JS使用的是垃圾回收,这就是当您说“由于black在函数内声明,不会被销毁” 时您似乎要指的是。还请记住,如果在函数中声明一个对象,然后将其分配给其他位置的变量,则该对象将保留,因为还有其他引用。
妮可(Nicole)

1
Objective-C(和Clang下的C)支持块,这些块本质上是闭包,没有垃圾回收。它需要运行时支持和一些有关内存管理的手动干预。
quixoto 2012年

68

闭包基本上只是查看对象的另一种方式。对象是绑定了一个或多个功能的数据。闭包是一种绑定了一个或多个变量的函数。至少在实现级别上,两者基本相同。真正的区别在于它们来自何处。

在面向对象的编程中,您可以通过预先定义其成员变量及其方法(成员函数)来声明对象类,然后创建该类的实例。每个实例都带有成员数据的副本,该副本由构造函数初始化。然后,您将拥有一个对象类型的变量,并将其作为一条数据传递出去,因为重点在于其作为数据的性质。

另一方面,在闭包中,对象不是像对象类那样预先定义的,也不是通过代码中的构造函数调用实例化的。相反,您可以将闭包编写为另一个函数内部的一个函数。闭包可以引用任何外部函数的局部变量,编译器会检测到该变量并将这些变量从外部函数的堆栈空间移至闭包的隐藏对象声明。然后,您将得到一个闭包类型的变量,即使它基本上是一个底层的对象,您也可以将其作为函数引用传递,因为重点在于其作为函数的性质。


3
+1:好答案。您可以将闭包视为仅使用一种方法的对象,将任意对象视为对某些常见基础数据(对象的成员变量)的闭包的集合。我认为这两种观点是相当对称的。
乔治

3
很好的答案。它实际上解释了关闭的见解。
RoboAlex

1
@Mason Wheeler:闭包数据存储在哪里?像函数一样在堆栈中?还是像对象一样堆放?
RoboAlex 2013年

1
@RoboAlex:在堆中,因为它是一个看起来像函数的对象。
梅森惠勒2013年

1
@RoboAlex:闭包及其捕获的数据的存储位置取决于实现。在C ++中,它可以存储在堆中或堆栈中。
Giorgio 2015年

29

术语“ 封闭”来自这样一个事实,即一段代码(块,函数)可以具有自由变量,这些自由变量被定义代码块的环境封闭(即绑定到一个值)。

以Scala函数定义为例:

def addConstant(v: Int): Int = v + k

在函数主体中,有两个名称(变量)vk指示两个整数值。名称v被绑定是因为它被声明为函数的参数addConstant(通过查看函数声明,我们知道v在调用函数时将为其分配值)。该k函数名称是免费的,addConstant因为该函数不包含k绑定到哪个值(以及绑定方式)的任何线索。

为了评估像这样的呼叫:

val n = addConstant(10)

我们必须分配k一个值,只有在k定义了上下文的名称中定义该值时才会发生addConstant。例如:

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  def addConstant(v: Int): Int = v + k

  values.map(addConstant)
}

现在,我们已经确定addConstant在上下文k的定义,addConstant已经成为一个封闭,因为它的所有自由变量正在关闭(绑定到一个值):addConstant可以调用,就好像它是一个功能传来传去。请注意,在定义k闭包时,free变量将绑定到一个值,而在调用闭包时,参数变量将被绑定。v

因此,闭包基本上是一个函数或代码块,可以在上下文绑定了自由变量后通过其自由变量访问非局部值。

在许多语言中,如果只使用一次闭包,则可以将其设为匿名,例如

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  values.map(v => v + k)
}

请注意,没有自由变量的函数是闭包的特殊情况(具有空的自由变量集)。类似地,匿名函数匿名闭包的特例,即匿名函数是没有自由变量的匿名闭包。


这与逻辑上的封闭式和开放式公式格格不入。感谢您的回答。
RainDoctor 2014年

@RainDoctor:自由变量在逻辑公式和lambda演算表达式中的定义方式相似:lambda表达式中的lambda就像逻辑公式中带有自由/绑定变量的量词一样。
Giorgio 2014年

9

JavaScript的简单说明:

var closure_example = function() {
    var closure = 0;
    // after first iteration the value will not be erased from the memory
    // because it is bound with the returned alertValue function.
    return {
        alertValue : function() {
            closure++;
            alert(closure);
        }
    };
};
closure_example();

alert(closure)将使用先前创建的值closure。返回的alertValue函数的名称空间将连接到closure变量所在的名称空间。当您删除整个函数时,该closure变量的值将被删除,但是在那之前,该alertValue函数将始终能够读取/写入variable的值closure

如果运行此代码,则第一次迭代将为closure变量赋值0 并将函数重写为:

var closure_example = function(){
    alertValue : function(){
        closure++;
        alert(closure);
    }       
}

并且由于alertValue需要局部变量closure来执行功能,因此它将自身与先前分配的局部变量的值绑定在一起closure

现在,每次调用该closure_example函数时,closure由于alert(closure)绑定,它将写出变量的增量值。

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 

谢谢,我没有测试代码=)现在一切似乎都很好。
Muha

5

本质上,“封闭”是将某些本地状态和某些代码组合到一个程序包中。通常,局部状态来自周围的(词法)范围,并且代码(本质上)是内部函数,然后返回给外部。然后,闭包是内部函数看到的已捕获变量和内部函数的代码的组合。

不幸的是,由于不熟悉,这是其中一件很难解释的事情。

我过去成功使用的一个比喻是:“想象一下,我们有一个叫做“书”的东西,在封闭的房间里,“那本书”是在TAOCP角落里的副本,但在封闭的桌子上,那是一本德累斯顿文件书的副本。因此,根据您所处的关闭状态,代码“给我书”会导致发生不同的事情。”


您忘记了这一点:答案中的en.wikipedia.org/wiki/Closure_(computer_programming)
S.Lott

3
不,我一直选择不关闭该页面。
Vatine 2011年

“状态和函数。”:具有static局部变量的C函数是否可以视为闭包?Haskell的关闭是否涉及国家?
Giorgio

2
Haskell中的@Giorgio Closures(我相信)在定义它们的词法范围内的论点附近,因此,我会说“是”(尽管我最多不熟悉Haskell)。带有静态变量的AC函数充其量只是一个非常有限的闭包(您确实希望能够通过一个函数创建一个带有static局部变量的闭包,而您只有一个)。
Vatine 2012年

我故意问这个问题,因为我认为带有静态变量的C函数不是闭包:静态变量是在本地定义的,并且仅在闭包内部才知道,它无法访问环境。另外,我不确定100%,但是我会反过来制定您的声明:您使用闭包机制创建不同的函数(函数是闭包定义+其自由变量的绑定)。
乔治

5

如果不定义“状态”的概念,就很难定义闭包是什么。

基本上,在具有完整词法作用域的语言中,将函数视为头等值,会发生一些特殊情况。如果我要执行以下操作:

function foo(x)
return x
end

x = foo

该变量x不仅引用function foo()而且还引用foo最后一次返回状态时保留的状态。真正的魔力是foo在其范围内进一步定义其他功能时发生的。这就像它自己的迷你环境(就像我们通常在全局环境中定义功能一样)。

从功能上讲,它可以解决许多与C ++(C?)的“静态”关键字相同的问题,该关键字在多个函数调用过程中保留局部变量的状态。但是,这更像是将相同的原理(静态变量)应用于函数,因为函数是一等值;闭包增加了对要保存的整个函数状态的支持(与C ++的静态函数无关)。

将函数视为第一类值并添加对闭包的支持还意味着您可以在内存中拥有多个相同函数的实例(类似于类)。这意味着您可以重用相同的代码,而不必重置函数的状态,这在处理函数内部的C ++静态变量时是必需的(这可能是错误的?)。

这是Lua关闭支持的一些测试。

--Closure testing
--By Trae Barlow
--

function myclosure()
    print(pvalue)--nil
    local pvalue = pvalue or 10
    return function()
        pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
        print(pvalue)
        pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
        return pvalue
    end
end

x = myclosure() --x now references anonymous function inside myclosure()

x()--nil, 20
x() --21, 31
x() --32, 42
    --43, 53 -- if we iterated x() again

结果:

nil
20
31
42

它可能会变得很棘手,并且可能因语言而异,但是在Lua中,似乎每当执行一个函数时,其状态都会被重置。我之所以这样说是因为,如果我们myclosure直接访问函数/状态(而不是通过返回的匿名函数),则上述代码的结果将有所不同,因为pvalue它将被重置回10;但是,如果我们通过x(匿名函数)访问myclosure的状态,pvalue则可以看到它处于活动状态并且在内存中的某个位置。我怀疑还有更多内容,也许有人可以更好地解释实现的本质。

PS:我不了解C ++ 11(以前版本中没有),因此请注意,这不是C ++ 11和Lua中的闭包之间的比较。同样,从Lua到C ++的所有“界线”都是相似的,因为静态变量和闭包不是100%相同。即使有时将它们用于解决类似问题。

我不确定的是,在上面的代码示例中,匿名函数还是高阶函数被视为闭包?


4

闭包是具有关联状态的函数:

在perl中,您可以这样创建闭包:

#!/usr/bin/perl

# This function creates a closure.
sub getHelloPrint
{
    # Bind state for the function we are returning.
    my ($first) = @_;a

    # The function returned will have access to the variable $first
    return sub { my ($second) = @_; print  "$first $second\n"; };
}

my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");

&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World

如果我们看一下C ++提供的新功能。
它还允许您将当前状态绑定到对象:

#include <string>
#include <iostream>
#include <functional>


std::function<void(std::string const&)> getLambda(std::string const& first)
{
    // Here we bind `first` to the function
    // The second parameter will be passed when we call the function
    return [first](std::string const& second) -> void
    {   std::cout << first << " " << second << "\n";
    };
}

int main(int argc, char* argv[])
{
    auto hw = getLambda("Hello");
    auto gw = getLambda("GoodBye");

    hw("World");
    gw("World");
}

2

让我们考虑一个简单的函数:

function f1(x) {
    // ... something
}

该函数称为顶级函数,因为它没有嵌套在任何其他函数中。每个 JavaScript函数都会将自己的对象列表关联到一个“作用域链”。该作用域链是对象的有序列表。这些对象中的每一个都定义了一些变量。

在顶级函数中,作用域链由单个对象(全局对象)组成。例如,f1上面的函数有一个范围链,其中有一个定义所有全局变量的对象。(请注意,这里的“对象”一词并不表示JavaScript对象,它只是一个实现定义的对象,它充当变量容器,JavaScript可以在其​​中“查找”变量。)

调用此函数时,JavaScript创建一个称为“激活对象”的东西,并将其放在作用域链的顶部。该对象包含所有局部变量(例如x此处)。因此,现在我们在范围链中有两个对象:第一个是激活对象,而在其下方是全局对象。

请非常小心地注意,这两个对象在不同的​​时间被放入作用域链中。定义函数时(即JavaScript解析函数并创建函数对象时)放置全局对象,并且在调用函数时进入激活对象。

因此,我们现在知道这一点:

  • 每个功能都有一个与其关联的作用域链
  • 定义函数时(创建函数对象时),JavaScript会使用该函数保存作用域链
  • 对于顶级功能,作用域链在功能定义时仅包含全局对象,并在调用时在顶部添加一个附加激活对象

当我们处理嵌套函数时,情况变得很有趣。因此,让我们创建一个:

function f1(x) {

    function f2(y) {
        // ... something
    }

}

f1被定义,我们得到了一个只包含全局对象是一个作用域链。

现在,当 f1被调用时,作用域链将f1获得激活对象。该激活对象包含变量xf2作为函数的变量。并且,请注意f2正在定义。因此,此时JavaScript也为保存了一个新的作用域链f2为此内部功能保存的作用域链是当前有效的作用域链。当前有效的作用域链是f1的。因此f2的范围链f1电流范围链-其中包含的激活对象f1和所述全局对象。

f2被调用时,它得到它自己的y包含激活对象,f1并添加到其作用域链中,该作用域已经包含和的激活对象。

如果在中定义了另一个嵌套函数f2,则它的作用域链在定义时将包含三个对象(两个外部函数的2个激活对象和全局对象),在调用时包含4个对象。

因此,现在我们了解范围链是如何工作的,但是我们还没有谈论闭包。

函数对象与范围(一组变量绑定)之间的组合(在该范围内解析函数的变量)在计算机科学文献中称为闭包-JavaScript,这是David Flanagan的权威指南

大多数函数是使用定义该函数时生效的作用域链来调用的,并且涉及闭包并不重要。当闭包在不同于定义时生效的作用域链下被调用时,它们变得很有趣。从定义嵌套函数的函数返回嵌套函数对象时,这种情况最常见。

函数返回时,该激活对象将从作用域链中删除。如果没有嵌套函数,则不会再有对激活对象的引用,它会被垃圾回收。如果定义了嵌套函数,则这些函数中的每一个都有对作用域链的引用,而该作用域链则指向激活对象。

但是,如果这些嵌套函数对象保留在其外部函数中,则它们本身将与所引用的激活对象一起被垃圾回收。但是,如果该函数定义了一个嵌套函数并将其返回或将其存储到某个位置的属性中,则将有对该嵌套函数的外部引用。它不会被垃圾收集,它所指向的激活对象也不会被垃圾收集。

在上面的示例中,我们没有f2从中返回f1,因此,当调用return 时f1,其激活对象将从其作用域链中删除并进行垃圾回收。但是,如果我们有这样的事情:

function f1(x) {

    function f2(y) {
        // ... something
    }

    return f2;
}

在这里,返回f2将具有范围链,该范围链将包含的激活对象f1,因此不会被垃圾回收。此时,如果我们调用f2,即使我们不在,它也将能够访问f1的变量。xf1

因此,我们可以看到一个函数保持了它的作用域链,并且作用域链伴随着外部函数的所有激活对象。这是关闭的本质。我们说JavaScript中的函数是“词法范围”的,这意味着它们保存了定义时处于活动状态的范围,而不是调用它们时处于活动状态的范围。

有许多强大的编程技术都涉及到闭包,例如近似私有变量,事件驱动的编程,部分应用程序等。

还要注意,所有这些都适用于所有支持闭包的语言。例如PHP(5.3 +),Python,Ruby等。


-1

闭包是编译器优化(又名语法糖?)。有些人也将其称为“ 穷人的对象 ”。

请参阅Eric Lippert的答案:(以下摘录)

编译器将生成如下代码:

private class Locals
{
  public int count;
  public void Anonymous()
  {
    this.count++;
  }
}

public Action Counter()
{
  Locals locals = new Locals();
  locals.count = 0;
  Action counter = new Action(locals.Anonymous);
  return counter;
}

说得通?
另外,您要求进行比较。VB和JScript都以几乎相同的方式创建了闭包。


这个答案是CW,因为我不配Eric的出色答案。请根据需要对其进行投票。HTH
goodguys_activate 2011年

3
-1:您的解释太扎根于C#。闭包在许多语言中都被使用,并且比这些语言中的语法糖更重要,并且包含功能和状态。
马丁·约克

1
不,闭包不仅是“编译器优化”,也不是语法糖。-1
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.