闭包的食用指南(一):何为闭包?
引言
在聊闭包之前,我想我们应该先理解何为「闭包」。以下是维基百科中的定义:
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
可以看出,相比于普通函数,闭包多了一个关联的环境。那么不难想到,关联的环境的相关概念便是闭包的重点。
为什么需要闭包?
下面,为了体会它与普通函数之间的区别,我们要引入2个简单的实际问题。
案例1:直线函数问题
第一个问题是用任意语言写一个直线函数:
我们先用一般的函数写法,代码如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import Foundation
func line(with k: Double, x: Double, b: Double) -> Double {
return k * x + b
}
// 输出y=2x,y=-3x,y=2这三条直线 在区间[0,10]上的函数值
for x in 0...10 {
let y1 = line(with: 2, x: Double(x), b: 0)
let y2 = line(with: -3, x: Double(x), b: 0)
let y3 = line(with: 0, x: Double(x), b: 2)
print("x=\(x), y1= \(y1), y2= \(y2), y3= \(y3)")
}
// 输出三条直线在x=5时的累加和
let sum = line(with: 2, x: 5, b: 0) + line(with: -3, x: 5, b: 0) + line(with: 0, x: 5, b: 2)
print("sum = \(sum)")
输出结果为: 1
2
3
4
5
6
7
8
9
10
11
12x=0, y1= 0.0, y2= 0.0, y3= 2.0
x=1, y1= 2.0, y2= -3.0, y3= 2.0
x=2, y1= 4.0, y2= -6.0, y3= 2.0
x=3, y1= 6.0, y2= -9.0, y3= 2.0
x=4, y1= 8.0, y2= -12.0, y3= 2.0
x=5, y1= 10.0, y2= -15.0, y3= 2.0
x=6, y1= 12.0, y2= -18.0, y3= 2.0
x=7, y1= 14.0, y2= -21.0, y3= 2.0
x=8, y1= 16.0, y2= -24.0, y3= 2.0
x=9, y1= 18.0, y2= -27.0, y3= 2.0
x=10, y1= 20.0, y2= -30.0, y3= 2.0
sum = -3.0
可以看出一些结论:
- 代码中定义了3条具体的直线函数(
、 、 )。 - 当要使用其中某一条直线时,每次都需在
函数中输入 、 值,有些累赘,可读性差,容易出错。
下面给出一个使用闭包的版本:
1 | import Foundation |
闭包版本与普通函数版本的输出结果一致,通过代码我们可以得出一些结论:
- 从数学角度来看,我们通过输入
、 到 getFx
,生成了一个新的直线函数f
。其中、 视为 f
中的常数,为自变量。 - 从程序角度来看,直线函数
f
捕获其定义域外的变量、 ,并保存在内存中,使得我们后续使用直线函数 f
时不需要重复输入这两个参数,这也就是前文所提到的关联的环境。将f
和其关联的环境视为一个整体,就可称为一个闭包 - 使用闭包后,代码更加简洁易懂,只需输入
即可,不容易出错。 - 不严谨地说,闭包看起来更像是一个对象。它包含一个函数,以及与该函数相关的成员变量(本例中是
、 )。
直线函数问题的柯里化写法
柯里化的概念说起来稍微有一些抽象,下面是维基百科中的定义:
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术
另外一句我觉得能够更为准确且易懂概括柯里化的意思:
在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。
其实在案例1中就有柯里化的味道了,不过不够彻底。因为柯里化每次可以固定一个参数,并以余下的参数为基础生成一个新函数。
从数学角度看,案例1中的闭包是由
如果要彻底柯里化,我们一次只固定一个参数。即从
下面是直线函数的柯里化写法:
1 | import Foundation |
通过上述代码,我们可以得出一些结论:
- 一开始我们固定了
和 ,分别得到了两个新函数 _6x
和_2x
,它们就是前文说的形式。 - 接着又固定了
、 、 、 ,分别得到了4个新函数 _6x_plus_2
、_6x_plus_7
、_2x_plus_5
、_2x_plus_0
,它们是前文说的形式。 - 可以看出,对直线函数柯里化后,可通过固定若干个参数得到很多新的函数。像
_6x
和_2x
还进一步生成了4个函数,这说明柯里化相比普通函数更加灵活。 - 与案例1一样,柯里化是闭包的一种应用,它也会捕获闭包函数定义域外的变量,也就是我们一直说的关联的环境。
案例2:表白打印机
需求:输入一个字符串数组到函数中,每次调用函数返回数组中下一个字符串元素,直到所有字符串都被返回。
例:输入["我","喜欢","你","❤️","!"],若调用5次函数,则依次输出"我"、"喜欢"、"你"、"❤️"、"!"。
我们首先还是用普通函数实现这个需求,作为闭包版本的参照物,代码如下:
1 | import Foundation |
输出结果:
1 | 我 |
通过上述代码,我们可以得到一些结论:
textPrinter
函数的主要逻辑:判断了index
范围的合法性,并返回位置为index
的字符串。index
的初始化、递增等逻辑写在外部,没有与函数融为一体,容易出错,不易于复用。
对普通函数进行了一番无情的批判,下面是闭包实现版本,可细细品味与普通函数的不同之处。
1 | import Foundation |
通过上述闭包版本代码,我们也可以得出一些结论:
makeTextPrinter
返回的匿名函数捕获了一个变量index
,延长了这个变量的生命周期,它也是我们一直强调的关联的环境。index
如成员变量一样与函数绑定在一起,形成一个闭包。index
的生命周期和闭包的生命周期一致。- 在闭包生命周期内,闭包内部可以持续修改被捕获的变量
index
,所以闭包看起来像一个对象。 index
的初始化、递增等逻辑在函数内部实现,这样更加清晰,也更容易被复用,具体可以与普通函数版本对比。