闭包的确切定义是什么?

17

我已经阅读过stackoverflow和其他来源上关于闭包的文章,但仍有一件事情让我感到困惑。从我所能整理出来的技术上的解释来看,闭包简单地说就是一个包含函数代码和该函数中绑定变量值的数据集。

换句话说,从我的理解来看,下面的C函数应该是一个闭包:

int count()
{
    static int x = 0;

    return x++;
}

然而,我阅读的所有内容似乎都暗示闭包必须以某种方式涉及将函数作为一等对象传递。此外,通常暗示闭包不是过程式编程的一部分。这是解决方案与其解决的问题过度关联的情况,还是我对确切定义存在误解?


1
维基百科在定义方面特别出色:http://en.wikipedia.org/wiki/Closure_%28computer_science%29 - Mehrdad Afshari
7
然而,维基百科的定义通常由对管道应力和晶体学着迷的人编写(即它们并不总是最容易理解的东西)。 - Robert Harvey
1
维基百科上的一篇文章引起了我的阅读。它说任何带有自由变量的一级函数都是闭包。但这并没有什么意义,因为这包括所有根据其参数返回值的一级函数。它是否意味着当函数使用自由变量来实例化其绑定变量时,它就成为了闭包? - Amaron
显然,C函数不是第一类的。 - Nosredna
是的,但我认为维基百科的定义是错误的。现在我明白了,我的整个问题都围绕着计算机科学中的“自由变量”与数学中的不同这一事实解决了。 - Amaron
8个回答

23

不,那不是一个闭包。你的例子只是一个返回静态变量递增结果的函数。

这是闭包的工作方式:

function makeCounter( int x )
{
  return int counter() {
    return x++;
  }
}

c = makeCounter( 3 );
printf( "%d" c() ); => 4
printf( "%d" c() ); => 5
d = makeCounter( 0 );
printf( "%d" d() ); => 1
printf( "%d" c() ); => 6
换句话说,不同的makeCounter()调用会产生不同的函数,并且这些函数在它们所“封闭”的词法环境中具有自己的变量绑定。
编辑:我认为像这样的示例比定义更容易理解闭包。但如果您想要一个定义,我会说,“闭包是函数和环境的组合。该环境包含在函数中定义以及创建函数时对函数可见的变量。只要该函数存在,这些变量必须保持对该函数可用。”

这确实是一个闭包,但我正在寻找什么是闭包以及什么不是闭包的严格定义。我给出的例子意在暗示我认为应该是一个闭包,但我知道它可能不是一个闭包。尽管如此,我会为你的清晰和有用的例子点赞,而且它还不涉及Lisp。 - Amaron

12

关于精确定义,建议查看维基百科条目。它非常好。我只是想用一个例子来澄清。

假设这是一段C#代码片段(旨在在列表中执行AND搜索):

List<string> list = new List<string> { "hello world", "goodbye world" };
IEnumerable<string> filteredList = list;
var keywords = new [] { "hello", "world" };
foreach (var keyword in keywords)
    filteredList = filteredList.Where(item => item.Contains(keyword));

foreach (var s in filteredList)  // closure is called here
    Console.WriteLine(s);

在C#中,这是一个常见的陷阱。如果你看一下Where里面的lambda表达式,你会发现它定义了一个函数,其行为取决于其定义处变量的值。就像把变量本身传递给函数,而不是变量的值。实际上,当调用此闭包时,它检索keyword变量在该时间的值。这个示例的结果非常有趣。它打印出两个"hello world"和"goodbye world",这不是我们想要的。发生了什么?正如我上面所说,我们使用lambda表达式声明的函数是一个闭包,它对keyword变量进行了封闭,因此发生了这种情况:

filteredList = filteredList.Where(item => item.Contains(keyword))
                           .Where(item => item.Contains(keyword)); 

在执行关闭操作时,keyword的值为"world",因此我们基本上是使用相同的关键字多次过滤列表。解决方案是:

foreach (var keyword in keywords) {
    var temporaryVariable = keyword;
    filteredList = filteredList.Where(item => item.Contains(temporaryVariable));
}

由于temporaryVariable的作用域限制在foreach循环的主体中,因此在每次迭代中,它都是一个不同的变量。实际上,每个闭包将绑定到不同的变量(这些是每次迭代时不同实例的temporaryVariable)。这次,它会给出正确的结果("hello world"):

filteredList = filteredList.Where(item => item.Contains(temporaryVariable_1))
                           .Where(item => item.Contains(temporaryVariable_2));

其中temporaryVariable_1在闭包执行时的值为"hello",temporaryVariable_2的值为"world"。请注意,闭包延长了变量的生命周期(它们的生命应该在循环的每次迭代后结束),这也是闭包的一个重要副作用。


从.NET Framework v4.5.2开始,第一个代码片段只输出“hello world”。filteredList是在两个代码片段中的第一个foreach循环后{System.Linq.Enumerable.WhereListIterator<string>},并在第二个foreach循环中解析。 - Jenn

6
据我了解,闭包还必须能够访问调用上下文中的变量。闭包通常与函数式编程相关联。语言可以具有来自不同类型编程视角的元素,如函数式、过程式、命令式、声明式等。它们得名于被封闭在指定的上下文中。它们也可能具有词法绑定,因为它们可以使用与上下文中使用的相同名称引用指定上下文。您的示例没有参考任何其他上下文,只有一个全局静态上下文。
来自维基百科 闭包会封闭自由变量(非本地变量)。

即使闭包没有使用那些变量,它也必须保留每一个可访问全局变量的副本,以防该全局变量发生更改,您是否明白? - Amaron
2
闭包可以访问定义上下文中的变量,而不是调用上下文。理论上,您可以在一个作用域中定义闭包并在另一个作用域中访问它,从而绕过作用域规则--尽管并非所有编程语言都允许这样做。 - J-16 SDiZ
@Amaron:不是“它自己的副本”——它是同一个副本。如果你修改一个,你就修改了另一个。(再次强调,并非所有编程语言都允许这样做,但这是闭包的定义方式) - J-16 SDiZ
嗯,如果它是一个闭包,它会在其定义的上下文中存储指向所有全局变量的指针? - Amaron
1
一个闭包会封闭住自由变量(即非局部变量)。我想这终于让我明白了。顺便说一下,这并不是维基百科上所说的,但应该是这样的。我之前感到困惑是因为每个例子都展示了超出作用域的自由变量,在函数外部被取消引用,从而在数学术语中不再是自由变量。 - Amaron
大多数编程语言都是词法作用域的,因此变量的“作用域”取决于语法而不是运行时行为。动态地说,这意味着通过良好的闭包支持,变量可以在一个点处处于作用域内,在下一个点处处于作用域外,然后再次处于作用域内。 - Paul

3

闭包是一种用于表示具有局部状态的过程/函数的实现技术。其中一种实现闭包的方法在SICP中有所描述,我将简要介绍它。

所有表达式(包括函数)都在“环境”中进行评估。一个环境是一系列“帧”。一个帧将变量名映射到值。每个帧还有一个指针指向其封闭的环境。一个函数在一个新的环境中被评估,该环境具有一个包含其参数绑定的帧。现在让我们看一个有趣的场景,想象一下我们有一个名为“累加器”的函数,当它被评估时,它将返回另一个函数:

// This is some C like language that has first class functions and closures.
function accumulator(counter) {
    return (function() { return ++counter; });
}

当我们评估以下代码时会发生什么?
accum1 = accumulator(0);

首先创建一个新的环境,并在其第一个frame中绑定一个整数对象(用于counter)。返回值是一个新函数,它绑定在全局环境中。通常情况下,一旦函数评估结束,新环境就会被垃圾回收。但这里不会发生这种情况。因为accum1需要访问变量counter,所以它保留了对该环境的引用。当调用accum1时,它将增加引用环境中counter的值。现在我们可以将accum1称为具有本地状态或闭包的函数。

我在我的博客 http://vijaymathew.wordpress.com 中描述了一些闭包的实际用途。(请参阅“危险的设计”和“消息传递”帖子)。


2

已经有很多回答了,但我会再添加一个。

Closure不是函数编程语言的特有概念。例如,在Pascal(以及相关语言)中,它们出现在嵌套过程中。标准C没有它们(目前还没有),但我记得有一个GCC扩展。

基本问题在于,嵌套的过程可能会引用在其父级中定义的变量。此外,父程序可能会将对嵌套过程的引用返回给其调用者。

即使这些变量因为父进程已退出而不存在,嵌套的过程仍然会引用父级别的本地变量 - 具体来说,是当执行函数引用行时这些变量所具有的值。

如果过程从未从父级别返回,该问题甚至也会出现 - 在不同的时间构建的对嵌套的过程的不同引用可能使用相同变量的不同过去值。

解决方法是,当引用嵌套函数时,它会被封装在一个“closure”中,其中包含它后面需要的变量值。

Python lambda是一个简单的函数式示例...

def parent () :
  a = "hello"
  return (lamda : a)

funcref = parent ()
print funcref ()

我的Python语言有些生疏,但我认为这是正确的。重点是嵌套函数(lambda)仍然引用局部变量a的值,即使在调用时parent已经退出。该函数需要一个地方来保存该值,直到需要使用它,这个地方称为闭包。

闭包有点像隐式的参数集合。


1

很棒的问题!鉴于面向对象编程(OOP)原则之一是对象具有行为和数据,闭包是一种特殊类型的对象,因为它们最重要的目的是它们的行为。也就是说,当我谈论它们的“行为”时,我指的是什么?

(这里很多内容都来自Dierk Konig的《Groovy in Action》,这是一本非常棒的书)

从最简单的层面上讲,闭包实际上只是一些代码,被封装成一个中性的对象/方法。它是一个方法,因为它可以接受参数并返回值,但它也是一个对象,因为你可以传递对它的引用。

用Dierk的话来说,想象一个信封,里面有一张纸。一个典型的对象会在这张纸上写下变量及其值,但闭包会写下一系列指令。假设这封信说:“把这个信封和信交给你的朋友。”

In Groovy: Closure envelope = { person -> new Letter(person).send() }
addressBookOfFriends.each (envelope)

这里的闭包对象是信封变量的值,它的用途是作为each方法的参数。

一些细节: 作用域:闭包的作用域是可以在其中访问的数据和成员。 从闭包返回:闭包通常使用回调机制来执行并从自身返回。 参数:如果闭包只需要一个参数,Groovy和其他语言提供了一个默认名称:"it",以加快编码速度。 因此,例如在我们之前的例子中:

addressBookOfFriends.each (envelope) 
is the same as:
addressBookOfFriends.each { new Letter(it).send() }

希望这就是你所寻找的!


1

一个对象是状态加函数。闭包是函数加状态。

当函数f闭合(捕获)x时,它就是一个闭包。


0

我认为Peter Eddy说得对,但是例子可以更有趣一些。你可以定义两个函数来封闭一个本地变量,分别是增加和减少。计数器将在这对函数之间共享,并且对它们是唯一的。如果你定义了一对新的增加/减少函数,它们将共享不同的计数器。

此外,你不需要传入x的初始值,你可以让它在函数块内默认为零。这将使它更清晰,它正在使用一个你无法正常访问的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接