闭包:为啥它们如此有用?
Posted
技术标签:
【中文标题】闭包:为啥它们如此有用?【英文标题】:Closures: why are they so useful?闭包:为什么它们如此有用? 【发布时间】:2010-11-21 07:00:15 【问题描述】:作为OO 开发人员,我可能很难看到它的价值。他们提供了什么附加值?它们适合 OO 世界吗?
【问题讨论】:
另见***.com/questions/256625/when-to-use-closure 【参考方案1】:你可以把它看作是一个类的泛化。
您的班级拥有某种状态。它有一些成员变量可供其方法使用。
闭包只是让函数访问本地状态的一种更方便的方式。
不必创建一个知道你希望函数使用的局部变量的类,你可以简单地在现场定义函数,它可以隐式访问当前可见的每个变量。
当你在传统的 OOP 语言中定义一个成员方法时,它的闭包是“所有在这个类中可见的成员”。
具有“适当”闭包支持的语言简单地概括了这一点,因此函数的闭包是“这里所有可见的变量”。如果“here”是一个类,那么你有一个传统的类方法。
如果“here”在另一个函数中,那么你就有了函数式程序员认为的闭包。您的函数现在可以访问在父函数中可见的任何内容。
所以这只是一个概括,消除了“函数只能在类中定义”的愚蠢限制,但保留了“函数可以看到在声明它们的点可见的任何变量”的想法。
【讨论】:
我认为情况正好相反:类是闭包的泛化。闭包只是一个可以访问一堆状态的函数,但是一个类有许多方法可以共享对同一状态的访问。许多语言(例如 Java、Python 等)都有本地类,它们可以像闭包一样从周围的作用域中捕获变量;所以它们严格来说比闭包更通用。 @newacct:一点也不。您可以使用相同的闭包轻松创建多个函数。只需在同一个地方创建它们,它们就会捕获相同的变量。但正如我所说,闭包捕获“这里可见的任何东西”,而类方法捕获“这里可见的任何东西并且在类中。当然,我不是在谈论本地类,这与“常规”课程没有太大关系。 @jalf 所以。方法是闭包?【参考方案2】:闭包不会给你任何额外的力量。
任何你可以通过他们实现的事情,你也可以在没有他们的情况下实现。
但是它们对于使代码更清晰和可读性非常有用。 而且我们都知道干净可读的短代码是一种更容易调试并且包含的错误更少的代码。
让我给你一个可能使用的简短 Java 示例:
button.addActionListener(new ActionListener()
@Override public void actionPerformed(ActionEvent e)
System.out.println("Pressed");
);
将被替换(如果 Java 有闭包):
button.addActionListener( System.out.println("Pressed"); );
【讨论】:
它们没有给你任何额外的力量——但它们确实给了你额外的风格......我知道我更愿意写和读你的第二个例子而不是前一个。如果你更进一步,考虑像 scala 这样的语言中的 map()、filter() 和 fold() 方法与它们的 java 替代品相比,风格变得明显 如果样式/可读性不是力量,那么我们可以用打孔卡编写世界上任何应用程序。 这个游戏有点晚了,但我不得不恭敬地不同意关闭不会给你任何额外的权力。在多线程环境中,执行闭包以在匿名函数中捕获局部范围的变量,然后在匿名函数执行时退出局部范围的能力非常强大。 这似乎是一流函数的示例,而不是闭包。闭包也意味着变量捕获。 这个例子是如何闭包的?我认为闭包必须引用封闭范围中的变量...【参考方案3】:对我来说,闭包的最大好处是当您编写的代码可以启动任务、让任务继续运行并指定任务完成后应该发生什么。通常,在任务结束时运行的代码需要访问开始时可用的数据,而闭包使这变得容易。
例如,javascript 中的一个常见用途是启动 HTTP 请求。启动它的人可能想要控制响应到达时发生的情况。所以你会做这样的事情:
function sendRequest()
var requestID = "123";
$.ajax('/myUrl',
success: function(response)
alert("Request " + requestID + " returned");
);
由于 JavaScript 的闭包,“requestID”变量被捕获在成功函数内部。这显示了如何在同一个地方编写请求和响应函数,并在它们之间共享变量。如果没有闭包,您需要将 requestID 作为参数传入,或者创建一个包含 requestID 和函数的对象。
【讨论】:
对不起,我在这里看到的是一个真实世界的反模式。在函数调用的参数列表中定义函数和对象 => 可读性差。当有人最终意识到将 requestID 作为参数传递更有意义时,您将遇到一个很难追踪的“有时”错误。仅当 ajax 调用在下一个请求之前没有返回时,匿名函数才会有错误的 ID。老实说,这样做的传统方式在可读性和健壮性方面更好,我真的想知道闭包的性能是否会好得多。 我同意没有必要在参数列表中定义函数——这正是我碰巧编写示例的方式。在另一个函数中定义它是重点。将 requestID 作为参数传递通常不是一种选择。例如,如果您使用第三方库发出请求,则无法控制它如何调用您的回调函数。 对不起,我弄糊涂了。如果函数被多次调用,则执行上下文实际上不会相同。我把循环和闭包的问题搞混了。【参考方案4】:恕我直言,它归结为能够捕获代码块和它们的上下文,以便稍后在某个时候引用并在/如果/需要时执行。
它们可能看起来没什么大不了的,而且闭包绝对不是您每天都需要完成的事情 - 但它们可以使代码代码更简单、更清晰/管理。
[编辑 - 基于上述评论的代码示例]
Java:
List<Integer> numbers = ...;
List<Integer> positives = new LinkedList<Integer>();
for (Integer number : integers)
if (number >= 0)
positives.add(number);
Scala(跳过了类型推断和通配符等其他一些细节,因此我们仅比较闭包的效果):
val numbers:List[Int] = ...
val positives:List[Int] = numbers.filter(i:Int => i >= 0)
【讨论】:
捕获上下文(以读写方式)是我不喜欢闭包的原因(与作为一流对象的函数相反):它增加了可能发生意外状态变化的地方. 同意——但强大的力量伴随着巨大的责任......如果你将闭包与尽可能多的不变性结合起来,你可以为自己省去很多痛苦,但这确实需要一点纪律(但是这样几乎没有滥用 C 的每个方面,所以这不是不可能的 :]) 我认为这就是闭包在函数式语言中如此流行的原因:该语言的无副作用结构可以防止愚蠢的代码。在 JavaScript 中,管理封闭上下文的规则是如此神秘,以至于大多数人只是假装它不存在。 嘿,不确定是否有“防止愚蠢的代码”之类的东西。有志者事竟成,但我明白你在说什么:)【参考方案5】:很遗憾,人们不再在edu中学习Smalltalk;在那里,闭包用于控制结构、回调、集合枚举、异常处理等等。 举一个很好的小例子,这是一个工作队列操作处理程序线程(在 Smalltalk 中):
|actionQueue|
actionQueue := SharedQueue new.
[
[
|a|
a := actionQueue next.
a value.
] loop
] fork.
actionQueue add: [ Stdout show: 1000 factorial ].
对于那些不会阅读 Smalltalk 的人,JavaScript 语法也是如此:
var actionQueue;
actionQueue = new SharedQueue;
function ()
for (;;)
var a;
a = actionQueue.next();
a();
;
.fork();
actionQueue.add( function () Stdout.show( 1000.factorial()); );
(嗯,如您所见:语法有助于阅读代码)
编辑:注意如何从块内部引用 actionQueue,即使对于分叉的线程块也有效。这就是使闭包如此易于使用的原因。
【讨论】:
【参考方案6】:这里有一些有趣的文章:
Crossing borders: Closures - Bruce Tate 提到了闭包的一些优点 A Definition of Closures - Neal Gafter(为 Java 发明了一项闭包提案的人之一)【讨论】:
第一个链接已经烂掉了。【参考方案7】:闭包非常适合 OO 世界。
以 C# 3.0 为例:它具有闭包和许多其他功能方面,但仍然是一种非常面向对象的语言。
根据我的经验,C# 的功能方面倾向于保留在类成员的实现中,而不是我的对象最终暴露的公共 API 的一部分。
因此,闭包的使用往往是其他面向对象代码中的实现细节。
我一直在使用它们,因为我们的一个单元测试(针对 Moq)中的这段代码 sn-p 显示:
var typeName = configuration.GetType().AssemblyQualifiedName;
var activationServiceMock = new Mock<ActivationService>();
activationServiceMock.Setup(s => s.CreateInstance<SerializableConfigurationSection>(typeName)).Returns(configuration).Verifiable();
如果没有 C# 的闭包特性,将输入值 (typeName) 指定为 Mock 期望的一部分将非常困难。
【讨论】:
【参考方案8】:关于结论性查询:“(闭包)是否适合 OO 世界?”
在许多语言中,闭包以及一流的函数为设计 OO 程序提供了非常基础:可以想到 Javascript、Lua 和 Perl。
在我熟悉的另一种更“传统”的 OO 语言 Java 中,有两个主要区别:
-
函数不是一流的构造
在 Java 中,OO 的实现细节(是否在内部使用闭包)不会像在 Javascript、Lua 和 Perl 中那样直接向开发人员公开
因此,在诸如 Java 之类的“传统 OO”语言中,添加闭包在很大程度上只是语法糖。
【讨论】:
【参考方案9】:也许在编译编程的世界中,闭包的好处不太明显。在 JavaScript 中,闭包非常强大。这有两个原因:
1) JavaScript 是一种解释性语言,因此指令效率和命名空间保护对于大型程序或评估大量输入的程序更快、响应更快的执行非常重要。
2) JavaScript 是一种 lambda 语言。这意味着在 JavaScript 中,函数是定义范围的第一类对象,并且子对象可以访问父函数的范围。这很重要,因为变量可以在父函数中声明,在子函数中使用,并且即使在子函数返回后也可以保留值。这意味着一个变量可以被函数多次重用,其值已由该函数的最后一次迭代定义。
因此,闭包非常强大,并且在 JavaScript 中提供了卓越的效率。
【讨论】:
【参考方案10】:看看上面的例子,我可以补充一下。
我欣赏风格、状态隐藏部分和表达链,但这还不够
关于简单明确的代码可以说很多
我觉得 Scala、Perl 和其他一些 Scheme 类型的语言旨在为语言设计者的特定思维方式提供一种简写方式,或者让它们“更有效率”我会认为 python 的简单性和 java 的早期语法等作为简单性和明确性的更好方式
在狭小的空间中编写神秘的链式闭包可能会非常快。然而一切都可以通过简单的对象和算法来完成
闭包是必要的,但并不经常保证它们是一等公民。
【讨论】:
确实如此。您可以在 c 中编写带有垃圾收集的对象系统。 c 与汇编程序只是一个简短的抽象。但是编写和维护有多容易?抽象很有用。被设计成一种语言的抽象被称为reified。 “需要关闭吗?”的问题。是恕我直言,错误的问题。我认为问题应该是,“可以用闭包编写富有表现力、更短和/或更清晰的代码吗?” 当我看到类似:“c is only a short abstraction away from assembler”时,我确信我在听一个从未接触过汇编程序的人。它们是两种截然不同的语言。以上是关于闭包:为啥它们如此有用?的主要内容,如果未能解决你的问题,请参考以下文章