Swift函数就是闭包。这意味着它们可以在函数体作用域中捕获对外部变量的引用。这是什么意思呢?回忆一下第1章,花括号中的代码构成了一个作用域,这些代码能够看到外部作用域中声明的变量与函数:
class Dog { var whatThisDogSays = /"woof/" func bark { print(self.whatThisDogSays) }}
在上述代码中,bark函数体引用了变量whatThisDogSays,该变量是函数体的外部变量,因为它声明在函数体外部。它位于函数体的作用域中,因为函数体内部的代码可以看到它。函数体内部的代码能够引用whatThisDogSays。
一切都很好;不过,我们现在知道函数bark可以当作值来传递。实际上,它可以从一个环境传递到另一个环境中!如果这样做,那么对whatThisDogSays的引用会发生什么情况呢?下面就来看看:
func doThis(f : Void -> Void) { f}let d = Dogd.whatThisDogSays = /"arf/"let f = d.barkdoThis(f) // arf
运行上述代码,控制台会打印出/"arf/"。
也许结果不会让你感到惊讶,不过请思考一下。我们并未直接调用bark。我们创建了一个Dog实例,然后将其bark函数作为值传递给函数doThis,然后被调用。现在,whatThisDogSays是某个Dog实例的一个实例属性。在函数doThis中并没有whatThisDogSays。实际上,在函数doThis中并没有Dog实例!不过,调用f()依然可以使用。函数d.bark还是可以看到变量whatThisDogSays(声明在外部),虽然它的调用环境中并没有任何Dog实例,也没有任何实例属性whatThisDogSays。
bark函数在传递时会持有其所在的环境,甚至在传递到另一个全新环境中再调用时亦如此。对于“捕获”,我的意思是当函数作为值被传递时,它会持有对外部变量的内部引用。这使得函数成为一个闭包。
你可能利用了函数就是闭包这一特性,但却根本就没有注意到过。回忆一下之前的示例,在界面上以动画的形式移动按钮的位置:
UIView.animateWithDuration(0.4, animations: { self.myButton.frame.origin.y += 20 }) { _ in print(/"finished!/")}
上述代码看起来很简单,但请注意第2行,匿名函数作为实参被传递给了animations:参数。真是这样的吗?这与Cocoa相差甚远,这个匿名函数会在未来的某个时间被调用来启动动画,Cocoa会找到myButton,这个对象是self的一个属性,代码中早就是这样的了?是的,Cocoa可以做到这一点,因为函数就是个闭包。对该属性的引用会被捕获并由匿名函数维护;这样,当匿名函数真正被调用时,它就会执行,按钮也会移动。
2.13.1 闭包是如何改善代码的
如果理解了函数就是闭包这一理念,那么你就可以利用这一点来改善代码的语法了。闭包会让代码变得更加通用,实用性也更强。下面这个函数是之前提及的一个示例,它接收绘制指令,然后执行来生成一张图片:
func imageOfSize(size:CGSize, _ whatToDraw: -> ) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0) whatToDraw let result = UIGraphicsGetImageFromCurrentImageContext UIGraphicsEndImageContext return result}
我们可以通过一个尾匿名函数来调用imageOfSize:
let image = imageOfSize(CGSizeMake(45,20)) { let p = UIBezierPath( roundedRect: CGRectMake(0,0,45,20), cornerRadius: 8) p.stroke}
不过,上述代码还有一个讨厌的重复情况。这是个会根据给定大小(包含该尺寸的圆角矩形)创建图片的调用。我们重复了这个尺寸;数值对(45,20)出现了两次,这么做可不好。下面将尺寸放到起始位置处的变量中来避免重复。
let sz = CGSizeMake(45,20)let image = imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke}
内部可以看到声明在更高层的匿名函数中的变量sz。这样,我们就可以在匿名函数中引用它了,我们也是这么做的。匿名函数就是个函数,因此也是闭包。匿名函数会捕获引用,将其放到对imageOfSize的调用中。当imageOfSize调用whatToDraw,而whatToDraw引用了变量sz时,这么做是没问题的,即便在imageOfSize中并没有sz也可以。
下面更进一步。到目前为止,我们硬编码了所需圆角矩形的大小。不过,假设创建各种大小的圆角矩形图片是经常性的事情,那么将代码放到函数中就是更好的做法,其中sz不是固定值,而是一个参数;接下来,函数会返回创建好的图片:
func makeRoundedRectangle(sz:CGSize) -> UIImage { let image = imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke } return image}
代码依然可以正常使用。匿名函数中的sz会引用传递给外围函数makeRounded-Rectangle的sz参数。外围函数的参数对于外部以及匿名函数都是可见的。匿名函数是个闭包,因此在传递给imageOfSize时它会捕获对该参数的引用。
代码现在变得很紧凑了。为了调用makeRoundedRectangle,提供一个尺寸即可;创建好的图片就会返回。这样,就可以执行调用,获取图片,然后将图片放到界面上,所有这些只需一步即可实现,如以下代码所示:
self.myImageView.image = makeRoundedRectangle(CGSizeMake(45,20)).
2.13.2 返回函数的函数
现在再进一步!相对于返回一张图片,函数可以返回一个函数,这个函数可以创建出指定大小的圆角矩形。如果从来没有见过一个函数可以以值的形式从另一个函数中返回,那么现在就是见证奇迹的时刻了。毕竟,函数可以当作值。在函数调用中,我们已经将函数作为实参传递给另一个函数了,现在来从一个函数中接收一个函数作为其结果:
func makeRoundedRectangleMaker(sz:CGSize) -> -> UIImage { ① func f -> UIImage { ② let im = imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke } return im } return f ③}
下面来分析一下上述代码:
①声明是最难理解的部分。函数makeRoundedRectangleMaker的类型(签名)到底是什么呢?它是(CGSize)-->()-->UIImage。该表达式有两个箭头运算符。为了理解这一点,请记住每个箭头运算符后面的内容就是返回值的类型。因此,makeRoundedRectangleMaker是个函数,它接收CGSize参数并返回a()--->UIImage。那()-->UIImage又是什么意思呢?我们其实已经知道了:它是个函数,不接收参数,并且返回一个UIImage。这样,makeRoundedRectangleMaker就是个函数,接收一个CGSize参数并返回一个函数,如果不传递参数,那么该函数本身就会返回一个UIImage。
②现在来看makeRoundedRectangleMaker函数体,首先声明一个函数(函数中的函数或是局部函数),其类型是我们期望返回的,即它不接收参数并返回一个UIImage。我们将该函数命名为f,该函数的工作方式非常简单:调用imageOfSize,传递一个匿名函数(创建一个圆角矩形图片im),然后将图片返回。
③最后,返回创建的函数(f)。我们已经实现了契约:返回一个函数,它不接收参数并返回一个UIImage。
也许你还对makeRoundedRectangleMaker感到好奇,想知道该如何调用它,以及调用之后会得到什么结果。下面就来试一下:
let maker = makeRoundedRectangleMaker(CGSizeMake(45,20))
代码运行后变量maker值是什么呢?它是个函数,不接收参数,当调用后会生成一张大小为(45,20)的圆角矩形图片。不相信?那我就来证明一下,调用这个函数(它现在是maker的值):
let maker = makeRoundedRectangleMaker(CGSizeMake(45,20))self.myImageView.image = maker
现在应该理解函数可以将函数作为结果了,下面来看看makeRoundedRectangleMaker的实现,再次对其进行分析,这次是以不同的方式。记住,我并没有向你演示函数可以产生函数,我这么写只是为了说明闭包!来看看环境是如何被捕获的:
func makeRoundedRectangleMaker(sz:CGSize) -> -> UIImage { func f -> UIImage { let im = imageOfSize(sz) { // * let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), // * cornerRadius: 8) p.stroke } return im } return f}
函数f不接收参数。不过,在f的函数体中(参见*注释)引用了尺寸值sz两次。f的函数体可以看到sz,它是外围函数makeRoundedRectangleMaker的参数,因为sz位于外围作用域中。函数f在makeRoundedRectangleMaker调用时捕获对sz的引用,并且在将f返回并赋给maker时保持该引用:
let maker = makeRoundedRectangleMaker(CGSizeMake(45,20))
这正是maker现在是一个函数的原因所在,当调用时,它会创建并返回一个尺寸为(45,20)的图片,虽然它本身被调用时并没有任何参数。我们已经将所要生成的图片尺寸传递给了maker。
从另一个角度再来看看,makeRoundedRectangleMaker是一个工厂,用于创建类似于maker的一系列函数,其中每个函数都会生成特定尺寸的一张图片。这是对闭包功能的最好说明。
继续之前,我准备以更加Swift的风格重写该函数。在函数f中,我们无须创建im再将其返回;可以直接返回调用imageOfSize的结果:
func makeRoundedRectangleMaker(sz:CGSize) -> -> UIImage { func f -> UIImage { return imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke } } return f}
不过没必要声明f再将其返回;可以将其定义为匿名函数,然后直接返回:
func makeRoundedRectangleMaker(sz:CGSize) -> -> UIImage { return { return imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke } }}
不过,匿名函数只包含了一条语句,返回imageOfSize的调用结果。(imageOfSize的匿名函数参数有很多行,不过imageOfSize调用本身只有一行Swift语句。)因此,没必要使用return:
func makeRoundedRectangleMaker(sz:CGSize) -> -> UIImage { return { imageOfSize(sz) { let p = UIBezierPath( roundedRect: CGRect(origin:CGPointZero, size:sz), cornerRadius: 8) p.stroke } }}
2.13.3 使用闭包设置捕获变量
闭包可以捕获其环境的能力甚至要超过之前所介绍的。如果闭包捕获了对外部变量的引用,并且该变量的值是可以修改的,那么闭包就可以设置该变量。
比如,我声明了下面这个简单的函数。它所做的是接收一个函数,该函数会接收一个Int参数,然后通过实参100调用该函数:
func pass100 (f:(Int)->) { f(100)}
仔细看看如下代码,猜猜运行结果会是什么:
var x = 0print(x)func setX(newX:Int) { x = newX}pass100(setX)print(x)
第1个print(x)显然会打印出0。第2个print(x)调用会打印出100!pass100函数进入我的代码,并修改变量x的值!这是因为传递给pass100的函数包含了对x的引用;它不仅包含了对其引用,还能够捕获它;而且不仅能捕获它,还会设置x,就像直接调用setX一样。
2.13.4 使用闭包保存捕获的环境
当闭包捕获其环境后,即便什么都不做,它也可以保存该环境。如下示例可能会颠覆你的三观——这是一个可以修改函数的函数。
func countAdder(f:->) -> -> { var ct = 0 return { ct = ct + 1 print(/"count is (ct)/") f }}
函数countAdder接收一个函数作为参数,结果也返回一个函数。它所返回的函数会调用它所接收的函数;此外,它会增加变量值,然后打印出结果。现在猜猜如下代码运行后的结果会是什么:
func greet { print(/"howdy/")}let countedGreet = countAdder(greet)countedGreetcountedGreetcountedGreet
上述代码首先定义函数greet,它会打印出/"howdy/",然后将其传递给函数countAdder。countAdder返回的是一个新函数,我们将其命名为countedGreet。接下来调用countedGreet 3次。下面是控制台的输出:
count is 1howdycount is 2howdycount is 3howdy
显然,countAdder向传递给它的函数增加了调用次数的功能。现在来想想:维护这个数量的变量到底是什么呢?在countAdder内部,它是个局部变量ct;不过,它并未声明在countAdder所返回的匿名函数中。这么做是故意的!如果声明在匿名函数中,那么每次调用countedGreet时都会将ct设置为0,这就达不到计数的目的了。相反,ct只会被初始化为0一次,然后会由匿名函数所捕获。这样,该变量就会被保存为countedGreet环境的一部分了。在某些奇怪的保留环境的情况下,它会位于countedGreet外面,这样每次调用countedGreet时,它的值都会增加。这正是闭包的强大之处。
这个示例(可以保存环境状态)还有助于说明函数是引用类型的。为了证明这一点,我先来个对比示例。对一个函数工厂方法的两次单独调用会生成两个函数,正如你期望的那样:
let countedGreet = countAdder(greet)let countedGreet2 = countAdder(greet)countedGreet // count is 1countedGreet2 // count is 1
在上述代码中,两个函数countedGreet与countedGreet2会分别维护各自的数量。仅仅是赋值或是参数传递就会生成对相同函数的新引用,下面就来证明这一点:
let countedGreet = countAdder(greet)let countedGreet2 = countedGreetcountedGreet // count is 1countedGreet2 // count is 2