“闭包”和“块”之间的区别是什么?

49

我发现很多人把「闭包(closure)」和「块(block)」这两个词混用,其中大部分人无法解释自己在谈论什么。

一些Java程序员(甚至是来自非常昂贵的咨询公司的程序员)称匿名内部类为「块(block)」和「闭包(closure)」,但我知道这不是真的。(你不能从定义它们的方法作用域中传递可变变量...)

我正在寻找:

  • 一个精确、计算机科学定义块(block)
  • 一个精确、计算机科学定义闭包(closure)
  • 阐述二者之间差异的内容。

请提供链接、文章或书籍参考资料


我不确定它们是否有精确的定义,例如 Apple 添加到 gcc 的闭包被称为“块”。 - cobbal
如果你在谈论[] - 它们是从Smalltalk进入Objective-C的,其中它们被编译成一个令人困惑命名为BlockClosure的实例。 - Dafydd Rees
2
他所说的不是Objective-C,而是苹果对C语言的扩展。在OSX 10.6 Snow Leopard中,苹果引入了一个名为Grand Central Dispatch的新并发库。它是一个基于任务的库,有点类似于微软的Task Parallel Library。GCD的基本思想是将一块代码交给库,然后库决定何时以及在哪个核心上执行。通常,您会使用函数指针来完成这项工作,但苹果认为这太丑陋了,因此他们使用了基本上是lambda表达式的C语言扩展,称之为blocks。 - Jörg W Mittag
6个回答

34

块(block)只是由语句和声明组成的代码片段,而闭包(closure)是一个真正的一等对象,一个包含块作为值的变量。

主要区别在于块仅仅是将指令组合在一起(例如while语句的主体),而闭包是一个包含可执行代码的变量。

如果你拥有一个闭包,通常可以将它作为参数传递给函数,进行柯里化和去柯里化,最重要的是可以调用它!

Closure c = { println 'Hello!' }
/* now you have an object that contains code */
c.call()

当然,闭包更加强大,它们是变量,可以用于定义对象的自定义行为(通常在编程中必须使用接口或其他面向对象编程方法)。
你可以将闭包看作是一个包含了函数内部操作的函数。
块很有用,因为它们允许变量的作用域。通常情况下,当你在作用域内定义一个变量时,你可以轻松地覆盖外部定义,并且新的定义只会在块的执行期间存在。
for (int i = 0; i < 10; ++i)
{
     int t = i*2;
     printf("%d\r\n", t);
}

t定义在块内(for语句的主体)中,仅在该块内有效。


for循环的例子并不通用,因为“闭包”这个术语在Javascript中使用得比其他语言更多,而且for循环块根本不封装它们的变量,除非仅限于函数声明。我猜你的例子是Java?请具体说明。 - Jon Davis
当一个块只是一段代码时......告诉一个疯狂的Ruby程序员。 - aaa90210
1
@Jack,你能否解释一下什么是真正的一等对象?例如,我们可以将一个块作为参数传递,可以从函数中返回它,也可以将其赋值给一个变量。 - Shardul
1
@Shardul:我不知道你的编程背景,但有些编程语言称“块”为lambda或闭包。传统上,块只是多个语句的封闭体,例如if的主体,因此您无法执行您所述的任何操作。 - Jack
为了成为闭包,对象必须捕获周围的词法作用域。否则它只是一个匿名函数。 - Mark Reed

19

一个代码块是语法上的一个概念 - 一组语句的逻辑单元(与闭包不同,更多地与作用域相关)。

if (Condition) {
    // Block here 
} 
else {
    // Another block
}

闭包与匿名函数或类相关 - 它是一个绑定到环境中的变量(及其变量)的代码片段的匿名(函数)对象。

def foo() {
   var x = 0
   return () => { x += 1; return x }
}

这里的 foo 返回了一个闭包!本地变量 x 通过该闭包保留,即使在 foo 终止后也是如此,并且可以通过调用返回的匿名函数来递增。

val counter = foo()
print counter() // Returns 2
print counter() // Return 3

请注意,这仅适用于Ruby,其中块(block)和闭包(closure)被视为类似,因为Ruby所谓的块闭包:

(1..10).each do |x|
    p x
end

在Ruby中,each方法接受一个闭包函数(带有参数x),这个闭包函数在Ruby中被称为


还有一个与Smalltalk混淆的问题。Ruby程序员称之为块(block),而Smalltalk称之为“BlockClosure”! - Dafydd Rees
1
实际上,在你的 Ruby 示例中,这个块不是闭包,因为它没有关闭任何东西。更有趣的例子可能是 a = 7; (1..10).each do |x| p a << x end 或类似的东西。 - Jörg W Mittag

8
这里存在很多混淆,因为有些术语有多个定义,并且有多个不同的东西会被混合在一起,只是因为它们通常一起出现。
首先,我们有“块”。这只是一个代码词汇块,它形成一个单元 - 比如循环体。如果语言实际上具有块范围,则可以定义仅存在于该代码块内部的变量。
其次,我们有可调用代码作为值类型。在函数式语言中,这些是函数值 - 有时称为“funs”、“匿名函数”(因为函数在值中找到,而不是分配给它的名称;你不需要名称来调用它们),或者“lambda”(源自Church's Lambda Calculus中用于创建它们的运算符)。它们可能被称为“闭包”,但它们并不自动成为真正的闭包;为了符合条件,它们必须封装(“关闭”)其创建周围的词法范围 - 也就是说,在函数本身的范围之外定义但在其定义范围内的变量在函数被调用时仍然可用,即使调用点在引用变量已经超出范围并且其存储已被回收之后。
例如,考虑以下JavaScript代码:

function makeClosure() {
  var x = "Remember me!";
  return function() {
    return "x='" + x + "'";
  }
}

// console.log(x); 
// The above is an error; x is undefined
var f = makeClosure();
console.log(f());
// The above outputs a string that includes x as it existed when f was created.

变量 x 只在函数 makeClosure 的主体内部定义;在该定义之外,它不存在。调用完 makeClosure 后,其中声明的 x 应该被删除。从大多数代码的角度来看,确实如此。但是,由 makeClosure 返回的函数是在存在 x 的情况下声明的,因此当您稍后调用它时,它仍然可以访问它。这使它成为一个真正的闭包。
您可以拥有不是闭包的函数值,因为它们不保留作用域。您还可以拥有部分闭包;PHP 的函数值仅保留必须在创建值时列出的特定变量。
您还可以拥有可调用的代码值,它们根本不代表整个函数。Smalltalk 将其称为“块闭包”,而 Ruby 则将其称为“procs”,尽管许多 Ruby 程序员只将它们称为“blocks”,因为它们是由 {...}do...end 语法创建的具体版本。使它们与 lambda(或“函数闭包”)不同的是,它们不会引入新的调用级别。如果块闭包中的主体代码调用 return,则它会从该块闭包所在的外部函数/方法返回,而不仅仅是块本身。
这种行为对于保留 R.D. Tennent 所称的“对应原则”至关重要,该原则指出您应该能够使用内联函数替换任何代码,该代码包含在主体中并立即调用。例如,在 Javascript 中,您可以将此代码替换为:

x=2
console.log(x)

使用这个:

(function(){x = 2;})();
console.log(x)

那个例子并不是很有趣,但是能够进行这种转换而不影响程序行为的能力在函数重构中起着关键作用。但是使用Lambda表达式后,一旦嵌入了return语句,原则就不再适用:

function foo1() {
  if (1) {
    return;
  }
  console.log("foo1: This should never run.")
}
foo1()
function foo2() {
  if (1) {
    (function() { return; })();
  }
  console.log("foo2: This should never run.")
}
foo2()

第二个函数与第一个不同,因为return只从匿名函数返回,而不从foo2返回,所以会执行console.log。这破坏了对应原则。
这就是为什么Ruby有procs和lambdas,即使对新手来说这一区别经常令人困惑。procs和lambdas都是Proc类对象,但它们的行为不同,如上所述:在lambda中,return只从函数体返回,但在proc中,它会从包含proc的方法返回。
def test
  p = proc do return 1 end
  l = lambda do return 1 end
  r = l[]
  puts "Called l, got #{r}, still here."
  r = p[]
  puts "Called p, got #{r}, still here?"
end

上面的test方法永远不会执行第二个puts,因为调用p会立即导致test返回(并带有返回值1)。如果Javascript有块级闭包,你可以做同样的事情,但它没有(尽管有提议添加它们)。

2

我认为闭包的概念比块更加通用和明确定义。如果已经有真正的闭包存在,我就不觉得需要块。 - Wei Qiu
@WeiQiu,正如我在上面的回答中所说的,return 的行为构成了块闭包作为与函数闭包分开实体的大部分基础。 - Mark Reed

1

这些术语在如今的Ruby中通常是一起使用的,尽管这些构造以前出现在Algol、Smalltalk和Scheme中。如果有标准的话,我会引用Ruby标准。

我不确定我是否能够回答你的确切问题,但我可以举例说明。如果你已经知道了,请见谅...

def f &x
  yield
  x
end

def g
  y = "block"
  t = f { p "I'm a #{y}" }
  y = "closure"
  t
end

t = g
t.call

而且...

$ ruby exam.rb
"I'm a block"
"I'm a closure"
$ 

所以一个块是附加到方法调用的匿名函数式代码序列。它在 Ruby API 中广泛使用。当您使创建匿名函数变得足够容易时,它们被证明对各种事情都很有用。

但请注意,在f返回后,g返回,我们通过从f(作为x)和从g(作为t)返回它来保留块。现在我们第二次调用块。同样,请注意,g()已经返回。但是该块引用了不存在的函数实例(和范围)中的局部变量?!并且它获得了y的新值?!

所以一个闭包是一个封闭其词法作用域的函数对象。它们非常具有挑战性,因为它们破坏了在函数调用实例中对局部变量非常有用的堆栈模型。


1. Ruby有各种不同的闭包函数对象,这只是其中之一。


这个回答中的代码片段是我不使用Ruby的完美例证。 - William Entriken

0

5

这是一个 整数

Int workDaysInAWeek = 5

这是一个整数变量,可以设置为另一个整数。(如果情况阻止您修改该值,则可以称为常量。)

而上述内容涉及数字,闭包涉及算法。分别区分闭包的区别也相当于上述问题。


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