在阅读了许多解释闭包的文章之后,我仍然缺少一个关键概念:为什么要写闭包? 程序员将执行什么样的特定任务,最好通过闭包来完成?
Swift中闭包的示例是访问NSUrl并使用反向地址解析器。这是一个这样的例子。不幸的是,这些课程只是关闭。他们没有解释为什么将代码解决方案写为闭包。
一个现实世界中编程问题的示例可能会触发我的大脑说:“啊哈,我应该为此写一个闭包”,这比理论上的讨论要有用得多。该站点上不乏理论讨论。
在阅读了许多解释闭包的文章之后,我仍然缺少一个关键概念:为什么要写闭包? 程序员将执行什么样的特定任务,最好通过闭包来完成?
Swift中闭包的示例是访问NSUrl并使用反向地址解析器。这是一个这样的例子。不幸的是,这些课程只是关闭。他们没有解释为什么将代码解决方案写为闭包。
一个现实世界中编程问题的示例可能会触发我的大脑说:“啊哈,我应该为此写一个闭包”,这比理论上的讨论要有用得多。该站点上不乏理论讨论。
Answers:
首先,没有闭包是没有不可能的。您始终可以用实现特定接口的对象替换闭包。这只是简洁和减少耦合的问题。
其次,请记住,闭包经常不恰当地使用,在这种情况下,简单的函数引用或其他构造会更清晰。您不应将所有示例视为最佳实践。
在使用高阶函数时,当您实际上需要传达状态时,闭包真正超越其他构造的地方就可以了,您可以将它变成单行,如Wikipedia页面上有关闭包的 javascript示例所示:
// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
return bookList.filter(
function (book) { return book.sales >= threshold; }
);
}
在这里,threshold
它从定义的地方到使用的地方都非常简洁自然。精确地将其范围限制为尽可能小。 filter
不必编写以允许传递客户端定义的数据(例如阈值)的可能性。我们不必仅仅为了传达这一小功能中的阈值而定义任何中间结构。它是完全独立的。
您可以不加闭包地编写此代码,但是它将需要更多代码,并且很难遵循。而且,JavaScript具有相当冗长的lambda语法。例如,在Scala中,整个函数体为:
bookList filter (_.sales >= threshold)
但是,如果您可以使用ECMAScript 6,那么借助粗箭头功能,甚至JavaScript代码也变得更加简单,并且实际上可以放在一行中。
const bestSellingBooks = (threshold) => bookList.filter(book => book.sales >= threshold);
在您自己的代码中,寻找可以生成大量样板的地方,仅用于将临时值从一个地方传递到另一个地方。这些是考虑用闭包替代的绝佳机会。
bestSellingBooks
代码和filter
代码都施加限制,例如特定的接口或用户数据自变量,以便能够传递threshold
数据。这将两个功能以不太可重用的方式联系在一起。
作为解释,我将从这个关于闭包的优秀博客文章中借用一些代码。它是JavaScript,但这是大多数谈论闭包的博客帖子使用的语言,因为闭包在JavaScript中是如此重要。
假设您想将数组呈现为HTML表。您可以这样做:
function renderArrayAsHtmlTable (array) {
var table = "<table>";
for (var idx in array) {
var object = array[idx];
table += "<tr><td>" + object + "</td></tr>";
}
table += "</table>";
return table;
}
但是,关于数组中每个元素的呈现方式,您都受JavaScript左右。如果要控制渲染,可以执行以下操作:
function renderArrayAsHtmlTable (array, renderer) {
var table = "<table>";
for (var idx in array) {
var object = array[idx];
table += "<tr><td>" + renderer(object) + "</td></tr>";
}
table += "</table>";
return table;
}
现在,您可以传递一个返回所需渲染的函数。
如果要在每个表行中显示运行总计怎么办?您将需要一个变量来跟踪总数,不是吗?闭包允许您编写渲染器函数,该函数关闭正在运行的total变量,并允许您编写可以跟踪正在运行的total的渲染器:
function intTableWithTotals (intArray) {
var total = 0;
var renderInt = function (i) {
total += i;
return "Int: " + i + ", running total: " + total;
};
return renderObjectsInTable(intArray, renderInt);
}
即使重复调用并退出,这里发生的魔术renderInt
仍然保留对total
变量的访问renderInt
。
在比JavaScript更传统的面向对象的语言中,您可以编写一个包含该总变量的类,然后将其传递而不是创建闭包。但是,关闭是一种更强大,清洁和优雅的方式。
的目的closures
仅仅是保持状态;因此,名称closure
-它关闭状态。为了便于进一步解释,我将使用Javascript。
通常你有一个功能
function sayHello(){
var txt="Hello";
return txt;
}
变量范围绑定到此函数的位置。因此,执行后变量txt
超出了范围。函数完成执行后,无法访问或使用它。
闭包是一种语言构造,如前所述,它允许保留变量的状态并因此扩展范围。
这在不同情况下可能很有用。一种用例是构造高阶函数。
在数学和计算机科学中,高阶函数(也是函数形式,函数或函子)是至少执行以下一项功能的函数:1
- 将一个或多个功能作为输入
- 输出一个功能
一个简单但不可否认并非太有用的示例是:
makeadder=function(a){
return function(b){
return a+b;
}
}
add5=makeadder(5);
console.log(add5(10));
您定义一个函数makedadder
,该函数将一个参数作为输入并返回一个函数。有一个外部函数function(a){}
和一个内部 函数function(b){}{}
。您还(隐式)定义了另一个函数,add5
作为调用高阶函数的结果makeadder
。makeadder(5)
返回一个匿名(内部)函数,该函数依次使用1个参数,并返回外部函数的参数与内部函数的参数之和。
的招是,在返回的内部函数,该函数的实际增加,外部函数的参数的(范围a
)被保留。add5
记得,参数a
是5
。
或者显示至少一个有用的示例:
makeTag=function(openTag, closeTag){
return function(content){
return openTag +content +closeTag;
}
}
table=makeTag("<table>","</table>")
tr=makeTag("<tr>", "</tr>");
td=makeTag("<td>","</td>");
console.log(table(tr(td("I am a Row"))));
另一个常见用例是所谓的IIFE =立即调用的函数表达式。在javascript中,伪造私有成员变量非常普遍。这是通过创建私有范围= 的函数来完成的closure
,因为它是在调用定义后立即生成的。结构是function(){}()
。注意()
定义后的括号。这样就可以将其用于显示模块模式的对象创建中。诀窍是创建一个作用域并返回一个对象,该对象在执行IIFE之后可以访问该作用域。
Addi的示例如下所示:
var myRevealingModule = (function () {
var privateVar = "Ben Cherry",
publicVar = "Hey there!";
function privateFunction() {
console.log( "Name:" + privateVar );
}
function publicSetName( strName ) {
privateVar = strName;
}
function publicGetName() {
privateFunction();
}
// Reveal public pointers to
// private functions and properties
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName( "Paul Kinlan" );
返回的对象具有对函数(例如publicSetName
)的引用,这些函数又可以访问“私有”变量privateVar
。
但是这些是Javascript更特殊的用例。
程序员将执行哪些特定任务,而闭包可以最好地完成该任务?
有几个原因。一个人可能对他来说很自然,因为他遵循一种功能范式。或使用Javascript:仅需依靠闭包来规避该语言的某些怪癖。
闭包主要有两个用例:
异步性。 假设您要执行需要一段时间的任务,然后在完成后执行一些操作。您可以使代码等待完成,从而阻止进一步的执行并可能使程序无响应,也可以异步调用您的任务并说“在后台开始此长任务,并在完成时执行此关闭操作”,闭包包含完成后要执行的代码。
回调。 根据语言和平台的不同,它们也称为“代理”或“事件处理程序”。这个想法是,您有一个可自定义的对象,该对象在某些明确定义的点将执行一个event,该事件运行由设置它的代码传入的闭包。例如,在程序的UI中,您可能有一个按钮,并为其提供了一个闭包,其中包含当用户单击按钮时要执行的代码。
闭包还有其他几种用途,但这是两个主要用途。
另外两个例子:
排序
大多数排序功能都是通过比较对象对来进行操作的。需要一些比较技术。将比较限制在特定的运算符上意味着比较不灵活的排序。更好的方法是接收比较函数作为排序函数的参数。有时无状态比较功能可以很好地工作(例如,对数字或名称列表进行排序),但是如果比较需要状态怎么办?
例如,考虑按到某个特定位置的距离对城市列表进行排序。一个丑陋的解决方案是将该位置的坐标存储在全局变量中。这使比较函数本身成为无状态的,但要付出全局变量的代价。
这种方法排除了多个线程同时根据它们到两个不同位置的距离对同一城市列表进行排序的情况。封闭位置的闭包可以解决此问题,并且不需要全局变量。
随机数
原始rand()
没有参数。伪随机数生成器需要状态。有些人(例如,梅森·Twister)需要很多状态。甚至是简单但可怕的rand()
所需状态。阅读有关新的随机数生成器的数学期刊论文,您将不可避免地看到全局变量。这对于该技术的开发人员来说很不错,而对调用者来说却不太好。将状态封装在结构中并将结构传递给随机数生成器是解决全局数据问题的一种方法。这是许多非OO语言使用的使随机数生成器可重入的方法。闭包对调用者隐藏该状态。闭包提供了简单的调用顺序rand()
和封装状态的可重入性。
随机数不只是PRNG。大多数想要随机性的人都希望随机性以某种方式分布。我将从以0到1之间随机抽取的数字开始,或者简称为U(0,1)。任何会产生0到某个最大值之间的整数的PRNG都可以;只需将随机整数除以最大值(作为浮点)即可。一种方便且通用的实现方法是创建一个以闭包(PRNG)和最大值作为输入的闭包。现在,我们有了一个通用且易于使用的U(0,1)随机生成器。
除了U(0,1),还有许多其他分布。例如,具有一定平均值和标准偏差的正态分布。我遇到的每个正态分布生成器算法都使用U(0,1)生成器。创建普通生成器的一种方便且通用的方法是创建一个将U(0,1)生成器,均值和标准偏差封装为状态的闭包。至少从概念上讲,这是一个采用闭包作为参数的闭包。
闭包等效于实现run()方法的对象,相反,可以使用闭包来模拟对象。
闭包的优点是可以在您希望使用函数的任何地方轻松使用它们:aka高阶函数,简单的回调(或策略模式)。您无需定义接口/类即可构建临时闭包。
对象的优点是可以进行更复杂的交互:多种方法和/或不同的接口。
因此,使用闭包或对象主要是样式问题。这是一个使闭包变得容易但不方便使用对象实现的示例:
(let ((seen))
(defun register-name (name)
(pushnew name seen :test #'string=))
(defun all-names ()
(copy-seq seen))
(defun reset-name-registry ()
(setf seen nil)))
基本上,您封装了只能通过全局闭包访问的隐藏状态:您无需引用任何对象,只需使用由三个函数定义的协议即可。
我相信supercat对以下事实的第一句话:在某些语言中,可以精确地控制对象的生存期,而对于闭包而言,情况并非如此。但是,在使用垃圾收集语言的情况下,对象的生存期通常是不受限制的,因此可以构建一个可以在不应该调用它的动态上下文中调用的闭包(在流之后从闭包读取)例如关闭)。
但是,通过捕获将保护闭包执行的控制变量来防止此类滥用非常简单。更准确地说,这是我的想法(在Common Lisp中):
(defun guarded (function)
(let ((active t))
(values (lambda (&rest args)
(when active
(apply function args)))
(lambda ()
(setf active nil)))))
在这里,我们使用一个函数指定function
符并返回两个闭包,它们都捕获了一个名为active
:
function
,仅当active
为true时action
到nil
,又名false
。代替(when active ...)
,当然可以有一个(assert active)
表达式,该表达式可能引发异常,以防在不应该调用闭包的情况下调用该闭包。另外,请记住,不安全的代码在使用不当时可能已经自行抛出异常,因此您很少需要这样的包装器。
这是您将如何使用它:
(use-package :metabang-bind) ;; for bind
(defun example (obj1 obj2)
(bind (((:values f f-deactivator)(guarded (lambda () (do-stuff obj1))))
((:values g g-deactivator)(guarded (lambda () (do-thing obj2)))))
;; ensure the closure are inactive when we exit
(unwind-protect
;; pass closures to other functions
(progn
(do-work f)
(do-work g))
;; cleanup code: deactivate closures
(funcall f-deactivator)
(funcall g-deactivator))))
注意,停用的闭包也可以赋予其他功能;这里,局部active
变量不共享f
和g
; 另外,除了active
,f
仅指obj1
且g
仅指obj2
。
supercat提到的另一点是,闭包可能导致内存泄漏,但是不幸的是,对于垃圾回收环境中的几乎所有情况都是如此。如果它们可用,则可以通过弱指针解决(闭包本身可以保留在内存中,但不会阻止对其他资源的垃圾收集)。
List<T>
在(假设的类)中,TemporaryMutableListWrapper<T>
并将其公开给外部代码,则可以确保如果使包装无效,则外部代码将不再具有处理的任何方式List<T>
。可以设计闭包以使其达到预期目的后允许失效,但是这并不方便。存在闭包以使某些模式方便使用,而保护它们所需的努力将使其无效。
还没有说什么,但也许是一个更简单的例子。
这是一个使用超时的JavaScript示例:
// Example function that logs something to the browser's console after a given delay
function delayedLog(message, delay) {
// this function will be called when the timer runs out
var fire = function () {
console.log(message); // closure magic!
};
// set a timeout that'll call fire() after a delay
setTimeout(fire, delay);
}
此处发生的是,当delayedLog()
被调用时,它会在设置超时后立即返回,并且该超时在后台不断计时。
但是,当超时超时并调用该fire()
函数时,控制台将显示message
最初传递给的delayedLog()
,因为它仍然可以fire()
通过闭包使用。您可以拨打任意数量的电话delayedLog()
,并发送不同的消息,每次都可以延迟,这将做对事情。
但是让我们想象一下JavaScript没有闭包。
一种方法是进行setTimeout()
阻塞(更像是“睡眠”功能),因此delayedLog()
直到超时用完,作用域才会消失。但是阻止一切并不是很好。
另一种方法是将message
变量放在其他作用域中,该delayedLog()
作用域消失后将可以访问该变量。
您可以使用全局变量,或者至少使用“更广范围的”变量,但是您必须弄清楚如何跟踪超时发生的消息。但这不能只是顺序的FIFO队列,因为您可以设置所需的任何延迟。因此它可能是“先进先出”或类似的东西。因此,您需要其他方法将定时函数与所需的变量绑定在一起。
您可以实例化一个超时对象,该对象将计时器与消息“分组”。一个对象的上下文或多或少是一个可扩展的范围。然后,您将使计时器在对象的上下文中执行,以便它可以访问正确的消息。但是您必须存储该对象,因为如果没有任何引用,它就会被垃圾回收(没有闭包,也就不会有隐式引用)。并且一旦对象触发了超时,就必须将其删除,否则它将一直存在。因此,您需要某种类型的超时对象列表,并定期检查它是否要删除“用过的”对象-否则这些对象会在列表中添加和删除自身,然后...
所以...是的,这变得越来越乏味。
幸运的是,您不必使用更广泛的范围,也不必为保留某些变量而纠缠对象。由于JavaScript具有闭包,因此您已经完全具有所需的作用域。一个使您可以message
在需要时访问变量的范围。因此,您可以摆脱delayedLog()
上面的写作。
message
被包含在的功能范围内fire
,因此在进一步的调用中被引用;但它确实是意外这么说。从技术上讲,它是一个关闭。无论如何+1;)
makeadder
上面看到了您的示例,在我看来,它看起来几乎相同。您返回一个“ curried”函数,该函数采用一个arg而不是两个;使用相同的方法,我创建了一个接受零参数的函数。我只是不退还而是将其传递给setTimeout
。
message
in fire
生成闭包。当调用setTimeout
它时,它利用了保留状态。
PHP可用于帮助以另一种语言显示真实示例。
protected function registerRoutes($dic)
{
$router = $dic['router'];
$router->map(['GET','OPTIONS'],'/api/users',function($request,$response) use ($dic)
{
$controller = $dic['user_api_controller'];
return $controller->findAllAction($request,$response);
})->setName('api_users');
}
所以基本上我正在注册一个将针对/ api / users URI执行的函数。这实际上是一个中间件功能,最终被存储在堆栈中。其他功能将被包装。就像Node.js / Express.js一样。
在依赖注入容器可用(通过使用条款)函数里面,当它被调用。可以创建某种路线操作类,但是事实证明,此代码更简单,更快速且更易于维护。
甲闭合是一块任意代码,包括变量,可以作为第一类数据进行处理。
一个简单的例子是很好的旧qsort:它是对数据进行排序的函数。您必须给它一个指向比较两个对象的函数的指针。因此,您必须编写一个函数。可能需要对该函数进行参数化,这意味着您要给它提供静态变量。这意味着它不是线程安全的。您在DS中。因此,您编写了一个使用闭包而不是函数指针的替代方法。您可以立即解决参数化问题,因为参数已成为闭包的一部分。您使代码更具可读性,因为您编写了如何直接将对象与调用排序功能的代码进行比较。
在很多情况下,您想要执行一些操作,这些操作需要大量的样板代码,以及一些需要修改的微小但必不可少的代码。通过编写一个函数避免了样板代码一次,需要一个封闭的参数和做它周围的所有样板代码,然后你可以调用这个函数,并通过代码来适应一种闭合。一种非常紧凑且易读的代码编写方式。
您具有一个功能,其中需要在许多不同的情况下执行一些非平凡的代码。这通常会产生代码重复或扭曲的代码,因此非平凡的代码将只出现一次。琐碎的:您将闭包分配给变量,并在需要时以最明显的方式调用它。
多线程:iOS / MacOS X具有执行诸如“在后台线程上执行此关闭”,“ ...在主线程上”,“ ...在主线程上(从现在起10秒钟)”之类的功能。它使多线程变得微不足道。
异步调用:这就是OP所看到的。任何访问互联网的电话,或任何可能花费时间的事情(例如读取GPS坐标),都是您迫不及待想要得到结果的地方。因此,您具有在后台执行操作的函数,然后传递闭包以告诉他们完成操作时该怎么做。
这是一个小开始。在产生紧凑,可读,可靠和高效的代码方面,闭包具有革命性的五种情况。
闭包是编写要使用的方法的简便方法。它节省了您声明和编写单独方法的工作。当方法仅使用一次且方法定义简短时,此方法很有用。由于无需指定函数名称,返回类型或访问修饰符,因此减少了键入的好处。同样,在阅读代码时,您不必在其他地方查找方法的定义。
以上是Dan Avidar的“理解Lambda表达式”的摘要。
这为我澄清了使用闭包,因为它阐明了替代方法(闭包与方法)以及每种方法的好处。
以下代码仅在安装过程中使用一次。在viewDidLoad下就地编写它可以节省在其他地方搜索它的麻烦,并缩短了代码的大小。
myPhoton!.getVariable("Temp", completion: { (result:AnyObject!, error:NSError!) -> Void in
if let e = error {
self.getTempLabel.text = "Failed reading temp"
} else {
if let res = result as? Float {
self.getTempLabel.text = "Temperature is \(res) degrees"
}
}
})
另外,它提供了一个异步过程来完成而不阻塞程序的其他部分,并且闭包将保留一个值,以便在后续函数调用中重用。
另一个关闭;这个抓住了一个价值...
let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)