“闭包”和“lambda”之间有什么区别?

982

有人能解释一下吗?我理解它们背后的基本概念,但我经常看到它们被交替使用,让我感到困惑。

既然我们在这里了,那么它们与常规函数有何不同呢?


115
Lambda是一种语言结构,指的是匿名函数;而闭包则是实现一等函数的技术手段,不论函数是否为匿名。很不幸,这两者经常被许多人混淆。 - Andreas Rossberg
相关:函数指针,闭包和Lambda - legends2k
1
有关 PHP 闭包,请参见 http://php.net/manual/en/class.closure.php。这不是 JavaScript 程序员所期望的。 - PaulH
2
SasQ的回答非常出色。在我看来,如果这个问题引导用户去查看那个答案,它会对SO的用户更有用。 - AmigoNico
16个回答

796

Lambda是一个匿名函数,没有名称的函数。在一些语言中,比如Scheme,它们等同于被命名的函数。事实上,函数定义会在内部被重写为将一个lambda绑定到一个变量上。在其他语言中,比如Python,虽然它们有一些(相对无关紧要的)区别,但它们在其它方面的行为方式是相同的。

Closure是任何一个能够封闭它所定义的环境的函数。这意味着它可以访问不在参数列表中的变量。例如:

def func(): return h
def anotherfunc(h):
   return func()

这将导致错误,因为func没有在anotherfunc闭合环境-h未定义。 func仅闭合全局环境。下面是有效的:

def anotherfunc(h):
    def func(): return h
    return func()

因为在这里,funcanotherfunc中定义,在Python 2.3及以上版本(或类似的版本)中,当它们几乎正确地使用闭包(但仍然无法进行变异)时,这意味着它会封闭anotherfunc的环境并且可以访问其中的变量。在Python 3.1+中,使用 nonlocal关键字也能使变异起作用。

另一个重要的点是,即使func不再在anotherfunc中被评估,它仍将继续封闭anotherfunc的环境。以下代码也可正常工作:

def anotherfunc(h):
    def func(): return h
    return func

print anotherfunc(10)()

这将打印10。

正如您所注意到的那样,这与lambda无关-它们是两个不同的(尽管相关的)概念。


1
我认为这个语句对于更好地理解很有用:“Python支持一种称为函数闭包的特性,这意味着在非全局作用域中定义的内部函数会记住它们在定义时所处的封闭命名空间。” 更多信息请参见:http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/ - user3885927
4
他们既是lambda又是闭包。Java之前通过匿名内部类就有了闭包,现在Lambda表达式使此功能语法更加简便。因此,新特性最相关的方面可能是现在有了lambda。称它们为lambda并不不正确,它们确实是lambda。Java 8作者选择不强调它们是闭包的原因我不清楚。 - Claudiu
3
由于Java 8的Lambda表达式不是真正的闭包,它们只是闭包的模拟。它们更类似于Python 2.3的闭包(没有可变性,因此需要引用的变量是“有效final”),并在内部编译为非闭包函数,这些函数将封闭作用域中引用的所有变量作为隐藏参数传递进去。 - Logan Pickup
12
@Claudiu 我认为提到特定的语言实现(Python)可能会使答案过于复杂化。这个问题完全与语言无关(也没有特定的语言标签)。 - Matthew
1
即使在3.1之前,Python也支持变异(例如,如果d是一个闭合的字典,则d[a] = b可以正常工作),它只是不支持名称重新绑定d = {a: b}创建一个本地变量并隐藏任何闭包变量)。 就Python而言,这些是完全独立的操作。 因此,在旧版本的Python中,您可以合理地想要使用可变性来做任何事情,只是更麻烦,因为您必须将所有内容都包装在可变字典中。 - Kevin
显示剩余7条评论

560

关于lambda和闭包的概念,即使是在这里提出的StackOverflow问题的答案中也存在着很多混淆。与其向那些从某些编程语言的实践中学习闭包或其他无知程序员询问,不如前往源头(一切始于此)。由于lambda和闭包源自于上世纪30年代甚至还没有第一台电子计算机的时期发明的Lambda演算,因此我说的就是这个源头

Lambda演算是世界上最简单的编程语言。你可以在其中做的唯一的事情有:

  • 应用:将一个表达式应用于另一个表达式,表示为f x。(将其视为一个函数调用,其中f是函数,x是其唯一参数)
  • 抽象:通过将希腊字母λ(lambda)作为前缀、然后是符号名称(例如x)、然后是点.来绑定出现在表达式中的符号,以标记该符号只是一个“空位”,等待填入值,可以理解为一个“变量”。这将把表达式转换为一个期望一个参数函数
    例如:λx.x+2接受表达式x+2,并告诉我们表达式中的符号x是一种绑定变量,可以用您提供的参数替换它。
    请注意,这种定义的函数是匿名的——它没有名称,因此您尚不能引用它,但是可以通过向其提供它正在等待的参数(记得应用吗?)立即调用它,如下所示:(λx.x+2) 7。然后,字面值表达式(在本例中为7)被替换为应用lambda的子表达式x+2中的x,因此您会得到7+2,然后按照常见算术规则缩减为9

因此,我们解决了其中一个谜团:
lambda就是上述示例中的匿名函数λx.x+2


在不同的编程语言中,函数抽象的语法(lambda)可能会有所不同。例如,在JavaScript中的写法如下:

function(x) { return x+2; }

然后您可以立即将其应用于某些参数,如下所示:

(function(x) { return x+2; })(7)

或者您可以将这个匿名函数(lambda)存储到某个变量中:

var f = function(x) { return x+2; }

这个操作实际上给它一个名字f,使得你可以在以后的多次引用中使用它,例如:

alert(  f(7) + f(10)  );   // should print 21 in the message box

但你不必给它命名。可以直接调用:

alert(  function(x) { return x+2; } (7)  );  // should print 9 in the message box
在LISP中,Lambda表达式的写法如下:
(lambda (x) (+ x 2))

你可以通过立即将参数应用于lambda来调用它:

(  (lambda (x) (+ x 2))  7  )


好的,现在是时候解决另一个谜团了:什么是闭包。 为了做到这一点,让我们谈谈 lambda 表达式中的符号(变量)。

如我所说,lambda 抽象所做的是将符号绑定到其子表达式中,使其成为可替换的参数。这样的符号称为“bound”。但如果表达式中还有其他符号呢?例如:λx.x/y+2。在此表达式中,符号 x 由前面的 lambda 抽象 λx. 绑定。但是另一个符号 y 则没有被绑定 - 它是“free”的。我们不知道它是什么和来自哪里,因此我们不知道它的“意义”和代表的“值”,因此我们不能评估该表达式,除非我们弄清楚 y 的含义。

事实上,其他两个符号 2+ 也是一样的。只是我们对这两个符号非常熟悉,以至于通常会忘记计算机不认识它们,我们需要通过在库或语言本身中定义它们来告诉它们的含义。

您可以将“free”符号视为在表达式之外的“surrounding context”中定义的符号,这称为其“environment”。环境可以是该表达式是其中一部分的更大表达式(正如 Qui-Gon Jinn 所说:“总有更大的鱼”;)),或者在某个库或语言本身中(作为“primitive”)。

这使我们将 lambda 表达式分为两类:

  • 封闭表达式:在这些表达式中出现的每个符号都由某个 lambda 抽象绑定。换句话说,它们是“self-contained”的;它们不需要任何周围的上下文来进行评估。它们也被称为组合子。
  • 开放表达式:这些表达式中的某些符号未被绑定 - 也就是说,它们中出现的某些符号是“free”的,并且需要某些外部信息,因此在提供这些符号的定义之前无法评估它们。

您可以通过提供环境来关闭一个“open”lambda表达式,该环境定义了所有这些自由符号,并将它们绑定到某些值上(这些值可能是数字、字符串、匿名函数即 lambdas 等等…)。

这里是 闭包 部分:
lambda表达式 的闭包是定义在外部上下文(环境)中的一组特定符号,它们为该表达式中的 自由符号 提供值,使它们不再是自由的。它将一个开放的Lambda表达式(仍然包含一些“未定义”的自由符号)转换为一个闭合的Lambda表达式,后者不再具有任何自由符号。

例如,如果你有以下Lambda表达式:λx.x/y+2,那么符号x是绑定的,而符号y是自由的,因此该表达式是开放的,除非你说明y的含义 (对于+2也是同样的情况),否则无法计算。但假设你还有一个环境像这样:

{  y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5  }

这个环境为我们的 Lambda 表达式(y+2)提供了所有“未定义”的(自由的)符号的定义,以及一些额外的符号(qw)。我们需要定义的符号是该环境的子集:

{  y: 3,
+: [built-in addition],
2: [built-in number]  }

这正是我们的 lambda 表达式的 闭包 :

换句话说,它 关闭 了一个开放的 lambda 表达式。这就是“闭包”一词最初的来源,也是为什么这个主题中许多人的答案都不完全正确的原因 :P


那么,他们错在哪里呢?为什么他们中的许多人会说闭包是内存中的某些数据结构,或者他们使用的语言的某些功能,或者为什么他们将闭包与 lambda 混淆了呢? :P

嗯,Sun/Oracle、Microsoft、Google 等公司的市场营销人员要为此负责,因为这就是他们在自己的语言(Java、C#、Go 等)中称呼这些结构的方式。他们经常把应该只是 lambda 的东西称为“闭包”。或者他们称“闭包”是一种特殊的技术,用于实现词法作用域,即函数可以访问在其定义时定义在其外部范围内的变量。他们经常说函数“封装”这些变量,也就是将其捕获到某些数据结构中,以防止它们在外部函数执行完成后被销毁。但这只是虚构的“民间词源”和营销,这只会让事情更加混乱,因为每个语言供应商都使用自己的术语。

而且更糟糕的是,因为他们所说的话总有一点真实性,这不允许您轻易地将其视为虚假的 :P 让我解释一下:

如果你想要实现一个使用 lambda 作为一等公民的语言,那么你需要允许它们使用其周围上下文中定义的符号(即在你的 lambda 中使用自由变量)。并且即使周围函数返回,这些符号也必须存在。问题是,这些符号绑定到某个函数的本地存储(通常在调用栈上),当函数返回时就不存在了。因此,为了使 lambda 以您期望的方式工作,您需要以某种方式“捕获”其外部上下文中的所有这些自由变量,并在其外部上下文消失时保存它们,即找到您的 lambda 的 闭包(它使用的所有这些外部变量)并将其存储在其他地方(通过复制或预先准备空间,在堆上而非栈上)。您用来实现此目标的实际方法是您语言的“实现细节”。这里重要的是 闭包,即需要在 lambda 的 环境中保存的 自由变量 的集合。

人们很快就开始将他们语言实现中用于实现闭包的实际数据结构称为“闭包”本身。该结构通常看起来像这样:

Closure {
   [pointer to the lambda function's machine code],
   [pointer to the lambda function's environment]
}

这些数据结构被作为参数传递给其他函数、从函数返回并存储在变量中,以表示lambda表达式,并允许它们访问其封闭环境以及运行在该上下文中的机器代码。但这只是实现闭包的一种(多种方式之一),并不是闭包本身。

如我上面所解释的,lambda表达式的闭包是其环境中赋值给该lambda表达式中自由变量的定义的子集,有效地"关闭"该表达式(将一个“开放”的lambda表达式,无法进行求值,转化为“闭合”的lambda表达式,因为其中所有的符号现在都已定义)。

除此之外,任何东西都只是程序员和语言供应商对这些概念真正根源不了解而产生的"装船崇拜"和"巫术魔法"。

希望这回答了您的问题。但如果您有任何后续问题,请随意在评论中提出,我会尽力更好地解释。


105
最佳答案应该是通用性的解释,而不是针对特定语言的解释。 - Shishir Arora
84
我很喜欢这种解释事物的方式。从最基础的开始,讲述事物是如何运作的,然后再说明当前存在的误解是如何产生的。这个答案需要置顶。 - Sharky
3
尽管Lambda演算对我来说感觉像机器语言,但我必须承认它是一种“发现”语言,与“创造”的语言相比。因此,Lambda演算不太受任意约定的限制,更适合捕捉现实的基本结构。我们可以在Linq、JavaScript、F#等具体的编程语言中找到更易接近/可理解的内容,但Lambda演算可以直接探究问题的核心,没有其他干扰。 - StevePoling
10
感谢您多次重申观点,每次措辞略有不同。这有助于加强理解。我希望更多的人能够这样做。 - johnklawlor
3
你说得对。这个回答中有很多错误和误导性/混淆的陈述,但其中有一些是正确的。首先,Lambda演算中没有闭包,因为Lambda演算中没有环境(cc @ap-osd)。顺便说一句,恭喜!现在Google在此搜索中会呈现你错误的定义。实际上,闭包是将Lambda表达式与其定义环境配对。没有拷贝或子集,它必须是原始框架本身(带有它的指针向上链),因为它不是关于值,而是关于绑定。 - Will Ness
显示剩余13条评论

182

当大多数人想到函数时,他们会想到命名函数

function foo() { return "This string is returned from the 'foo' function"; }

当然,这些是按名称调用的:

foo(); //returns the string above

使用lambda表达式,您可以拥有匿名函数

 @foo = lambda() {return "This is returned from a function without a name";}

通过上面的例子,你可以通过分配给它的变量调用lambda函数:
foo();

比起将匿名函数赋值给变量,将它们传递给高阶函数(即接受/返回其他函数的函数)更有用。在许多这样的情况下,给函数命名是不必要的:
function filter(list, predicate) 
 { @filteredList = [];
   for-each (@x in list) if (predicate(x)) filteredList.add(x);
   return filteredList;
 }

//filter for even numbers
filter([0,1,2,3,4,5,6], lambda(x) {return (x mod 2 == 0)}); 

闭包可以是一个具名函数或匿名函数,在定义函数的作用域中“捕获”变量,因此当使用闭包自身的外部变量时,它被称为闭包。下面是一个具名闭包:

@x = 0;

function incrementX() { x = x + 1;}

incrementX(); // x now equals 1

这看起来似乎不多,但如果这全部在另一个函数中,并且您将 incrementX 传递给外部函数呢?

function foo()
 { @x = 0;

   function incrementX() 
    { x = x + 1;
      return x;
    }

   return incrementX;
 }

@y = foo(); // y = closure of incrementX over foo.x
y(); //returns 1 (y.x == 0 + 1)
y(); //returns 2 (y.x == 1 + 1)

这是如何在函数式编程中获取有状态对象的方法。由于不需要命名“incrementX”,因此您可以在此情况下使用lambda表达式:
function foo()
 { @x = 0;

   return lambda() 
           { x = x + 1;
             return x;
           };
 }

17
我使用的是什么语言? - Claudiu
8
这基本上是伪代码。其中包含一些Lisp和JavaScript,以及我正在设计的一种语言叫做“@”(“at”),以变量声明运算符命名。 - Mark Cidade
3
@MarkCidade,这种语言在哪里呢?有文档和下载吗? - Pacerier
5
为什么不在JavaScript中添加一个以 "@" 符号开头声明变量的约束条件呢?这样可以节省一些时间 :) - Nemoden
6
@Pacerier:我已经开始实现这个语言了:http://github.com/marxidad/At2015 - Mark Cidade
显示剩余3条评论

61
不是所有闭包都是lambda表达式,也不是所有lambda表达式都是闭包。两者都是函数,但并不一定按照我们所熟悉的方式声明。lambda表达式本质上是内联定义的函数,而不是标准的函数声明方法。lambda表达式经常可以作为对象传递。闭包是通过引用其外部字段来封闭其周围状态的函数。封闭的状态在调用闭包时保持不变。在面向对象的语言中,闭包通常通过对象提供。然而,一些面向对象的语言(例如C#)实现了更接近于纯函数式语言(如lisp)提供的闭包定义的特殊功能,这些语言没有封闭状态的对象。有趣的是,在C#中引入Lambda和Closure将函数式编程带入了主流使用。

那么,我们可以说闭包是lambda的子集,而lambda又是函数的子集吗? - sker
闭包是lambda的一个子集...但是lambda比普通函数更特殊。就像我说的,lambda是内联定义的。实际上,除非将它们传递给另一个函数或作为返回值返回,否则无法引用它们。 - Michael Brown
20
Lambda和闭包都是所有函数的子集,但是它们之间只有一个交集。闭包中不相交的部分被称为闭包函数,而不相交的Lambda则是具有完全绑定变量的自包含函数。请注意,本文只要求翻译,不包括解释或其他额外内容。 - Mark Cidade
1
咆哮...一些事实:(1)闭包不一定是函数。(2)Lisp不是纯函数式的。(3)Lisp 确实有对象;在“对象”的定义被覆盖为其他内容之前,它通常将“对象”视为“值”的同义词(例如通过CLOS)。 - FrankHB
你说得对...自从14年前我第一次写这个以来,我学到了很多东西。不知道能否修改以涵盖我所学到的内容。 - Michael Brown

20

简单来说:lambda是一种语言结构,即匿名函数的语法;闭包则是一种实现它(或者任何一级函数,无论是具名还是匿名)的技术。

更准确地说,闭包用于在运行时表示一个一级函数,它由其“代码”和“环境”构成,该环境“封闭”了该代码中使用的所有非本地变量。这样,在外部作用域已经退出时,那些变量仍然是可访问的。

不幸的是,有许多语言不支持将函数作为一级值,或者只以受限的形式支持它们。因此,人们通常使用术语“闭包”来区分“真正的东西”。


13

从编程语言的角度来看,它们是完全不同的两个东西。

基本上对于一个图灵完备的语言,我们只需要非常有限的元素,例如抽象、应用和规约。抽象和应用提供了构建lambda表达式的方式,而规约确定了lambda表达式的含义。

Lambda提供了一种将计算过程抽象化的方式。例如,要计算两个数字的和,可以将接受两个参数x、y并返回x+y的过程抽象出来。在scheme中,您可以写成:

(lambda (x y) (+ x y))
您可以更改参数的名称,但它完成的任务不会改变。在几乎所有的编程语言中,您都可以给lambda表达式取一个名字,这些被称为命名函数。但几乎没有任何区别,从概念上来说,它们只是一种语法糖。
好了,现在想象一下如何实现这个。每当我们将lambda表达式应用于某些表达式时,例如:
((lambda (x y) (+ x y)) 2 3)
我们可以将参数替换为要评估的表达式。这个模型已经非常强大了。但是,这个模型不能改变符号的值,例如我们无法模拟状态的变化。因此,我们需要一个更复杂的模型。 简而言之,每当我们想计算lambda表达式的含义时,我们将符号对和相应的值放入环境(或表格)中。然后,通过在表格中查找相应的符号来评估剩余部分(+ x y)。 现在,如果我们提供一些直接操作环境的原语,我们就可以模拟状态的变化!
有了这个背景,检查一下这个函数:
(lambda (x y) (+ x y z))

我们知道,当我们评估lambda表达式时,x y将被绑定在一个新的表中。但是我们应该如何查找变量z呢?实际上,z被称为自由变量。这意味着必须有一个外部环境包含z,否则仅绑定x和y是无法确定表达式的含义的。为了清楚起见,你可以在Scheme中编写以下内容:

((lambda (z) (lambda (x y) (+ x y z))) 1)

因此,在外部表中,z将绑定为1。我们仍然得到一个接受两个参数的函数,但它的真正含义也取决于外部环境。 换句话说,外部环境会封闭自由变量。借助set!,我们可以使函数具有状态性,即它不是数学意义上的函数。它的返回值不仅取决于输入,还取决于z。

这是你非常熟悉的东西,对象的方法几乎总是依赖于对象的状态。这就是为什么有些人说“闭包是穷人的对象”的原因。但我们也可以认为对象是闭包的穷人版本,因为我们非常喜欢头等函数。

我使用Scheme来说明这些想法,因为Scheme是最早具有真正闭包的语言之一。所有这里的材料在SICP第3章中都有更好的呈现。

总而言之,λ和闭包是真正不同的概念。λ是一个函数。闭包是一个λ和相应环境的对,它封闭了λ。


那么,我们可以通过嵌套lambda表达式来替换所有的闭包,直到没有自由变量为止?在这种情况下,我认为闭包可以被看作是一种特殊类型的lambda表达式。 - Trilarion
一些问题。(1) 这里的“约简”似乎不太清晰。在术语重写系统中,lambda抽象也是redex的实例,并且根据Scheme的规则将被重写为过程的值。你是指“变量引用”吗?(2) 抽象并非使语言图灵完备所必需的,例如组合逻辑没有抽象。(3) 许多现代语言中的命名函数是独立于lambda表达式构建的。其中一些具有lambda表达式不共享的奇特特性,例如重载。 - FrankHB
(4)在Scheme中,对象只是值。最好避免混合不明确的术语。(5)一个闭包不需要存储抽象的语法元素(还有其他运算符可以是抽象),因此一个闭包不是一个包含任何“lambda”东西的对。 (比声称“闭包是函数”的答案更正确,但仍需注意。) - FrankHB

11

这个概念与上述描述相同,但如果你来自PHP背景,以下使用PHP代码进一步解释。

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });

function ($v) { return $v > 2; }

是lambda函数的定义。我们甚至可以将其存储在变量中,以便重复使用。在IT技术中,lambda函数通常用于简化代码和提高效率。
$max = function ($v) { return $v > 2; };

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);

现在,如果你想改变过滤数组中允许的最大数值,你需要编写另一个lambda函数或者创建一个闭包(PHP 5.3):

$max_comp = function ($max) {
  return function ($v) use ($max) { return $v > $max; };
};

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max_comp(2));

闭包是在其自己的环境中评估的函数,该环境具有一个或多个绑定变量,当调用函数时可以访问这些变量。它们来自于函数式编程世界,其中有许多概念在发挥作用。闭包类似于lambda函数,但更加智能,因为它们具有与闭包定义的外部环境中的变量交互的能力。
以下是PHP闭包的一个简单示例:
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };

$closure();

这篇文章很好地解释了匿名或Lambda函数在PHP中的使用。


9
这个问题比较老,有许多答案。
现在随着Java 8和官方Lambda的非官方闭包项目,这个问题重新出现了。
在Java语境中的答案(来自Lambda和闭包-有什么区别?):
“闭包是一个lambda表达式和一个环境配对,将其自由变量绑定到一个值。在Java中,lambda表达式将通过闭包实现,因此这两个术语在社区中已经可以互换使用。”

Lamdas在Java中是如何通过闭包实现的?这是否意味着Lamdas表达式会被转换为旧式匿名类? - hackjutsu

6

Lambda vs Closure

Lambda是一个匿名的函数(方法)。

Closure是一个函数,它从其封闭作用域(如非局部变量)中捕获变量。

Java

interface Runnable {
    void run();
}

class MyClass {
    void foo(Runnable r) {

    }

    //Lambda
    void lambdaExample() {
        foo(() -> {});
    }

    //Closure
    String s = "hello";
    void closureExample() {
        foo(() -> { s = "world";});
    }
}

Swift[闭包]

class MyClass {
    func foo(r:() -> Void) {}
    
    func lambdaExample() {
        foo(r: {})
    }
    
    var s = "hello"
    func closureExample() {
        foo(r: {s = "world"})
    }
}

闭包示例中最好不要使用匿名函数,使用匿名类会更清晰。 - darw
或者可以使用命名函数来替代在两个示例中都使用匿名函数的闭包示例。 - darw

6

简单来说,闭包是关于作用域的技巧,lambda是一个匿名函数。我们可以使用lambda更优雅地实现闭包,并且lambda经常被用作传递给更高级函数的参数。


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