引言

在聊闭包之前,我想我们应该先理解何为「闭包」。以下是维基百科中的定义:

闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。

可以看出,相比于普通函数,闭包多了一个关联的环境。那么不难想到,关联的环境的相关概念便是闭包的重点。

为什么需要闭包?

下面,为了体会它与普通函数之间的区别,我们要引入2个简单的实际问题。

案例1:直线函数问题

第一个问题是用任意语言写一个直线函数:

我们先用一般的函数写法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 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
12
x=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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation

// 获得一个直线函数
func getFx(with k: Double, b: Double) -> ((Double) -> Double) {

func f(x: Double) -> Double {
return k * x + b
}
return f
}
// 递增的线
let increasingLine = getFx(with: 2, b: 0)
// 递减的线
let decreasingLine = getFx(with: -3, b: 0)
// 水平线
let horizontalLine = getFx(with: 0, b: 2)

// 输出0~10的函数结果
for x in 0...10 {
// 闭包函数捕获并保存了k,b的值,所以后续使用无需重复输入。
// k, b 就是前面提到的关联环境。
let y1 = increasingLine(Double(x))
let y2 = decreasingLine(Double(x))
let y3 = horizontalLine(Double(x))
print("x=\(x), y1= \(y1), y2= \(y2), y3= \(y3)")
}

// 输出三条直线在x=5时的累加和
let sum = increasingLine(5) + decreasingLine(5) + horizontalLine(5)
print("sum = \(sum)")

闭包版本与普通函数版本的输出结果一致,通过代码我们可以得出一些结论:

  • 从数学角度来看,我们通过输入getFx,生成了一个新的直线函数f。其中视为f中的常数,为自变量。
  • 从程序角度来看,直线函数f捕获其定义域外的变量,并保存在内存中,使得我们后续使用直线函数f时不需要重复输入这两个参数,这也就是前文所提到的关联的环境。将f和其关联的环境视为一个整体,就可称为一个闭包
  • 使用闭包后,代码更加简洁易懂,只需输入即可,不容易出错。
  • 不严谨地说,闭包看起来更像是一个对象。它包含一个函数,以及与该函数相关的成员变量(本例中是)。
直线函数问题的柯里化写法

柯里化的概念说起来稍微有一些抽象,下面是维基百科中的定义:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

另外一句我觉得能够更为准确且易懂概括柯里化的意思:

在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。

其实在案例1中就有柯里化的味道了,不过不够彻底。因为柯里化每次可以固定一个参数,并以余下的参数为基础生成一个新函数。

从数学角度看,案例1中的闭包是由出发,固定两个参数得到一个

如果要彻底柯里化,我们一次只固定一个参数。即从出发,固定得到函数,若再固定可以得到。可以看出,柯里化比我们案例1中的闭包多了一个函数

下面是直线函数的柯里化写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Foundation

// 获得一个直线函数
func getFx(with k: Double, b: Double) -> ((Double) -> Double) {

func f(x: Double) -> Double {
return k * x + b
}
return f
}

func curryGetFx(with k: Double) -> (Double) -> (Double) -> Double {
// 采用匿名函数写法
// k b x
return { b in
return { x in
return k * x + b
}
}
}

let _6x = curryGetFx(with: 6) // 固定k=6
let _2x = curryGetFx(with: 2) // 固定k=2

let _6x_plus_2 = _6x(2) // 固定k=6, b = 2
let _6x_plus_7 = _6x(7) // 固定k=6, b = 10
let _2x_plus_5 = _2x(5) // 固定k=2, b = 5
let _2x_plus_0 = _2x(0) // 固定k=2, b = 0

print(_6x_plus_2(1)) // 输出8
print(_6x_plus_2(2)) // 输出14
print(_2x_plus_5(3)) // 输出11
print(_6x_plus_7(2)) // 输出19
print(_2x_plus_0(5)) // 输出10
print(_2x_plus_0(10)) // 输出20

通过上述代码,我们可以得出一些结论:

  • 一开始我们固定了,分别得到了两个新函数_6x_2x,它们就是前文说的形式。
  • 接着又固定了,分别得到了4个新函数_6x_plus_2_6x_plus_7_2x_plus_5_2x_plus_0,它们是前文说的形式。
  • 可以看出,对直线函数柯里化后,可通过固定若干个参数得到很多新的函数。像_6x_2x还进一步生成了4个函数,这说明柯里化相比普通函数更加灵活。
  • 与案例1一样,柯里化是闭包的一种应用,它也会捕获闭包函数定义域外的变量,也就是我们一直说的关联的环境
案例2:表白打印机

需求:输入一个字符串数组到函数中,每次调用函数返回数组中下一个字符串元素,直到所有字符串都被返回。

例:输入["我","喜欢","你","❤️","!"],若调用5次函数,则依次输出"我"、"喜欢"、"你"、"❤️"、"!"。

我们首先还是用普通函数实现这个需求,作为闭包版本的参照物,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

func textPrinter(with texts: [String], index: Int) -> String? {
if index < texts.count {
return texts[index]
}
return nil
}

var index = 0
let texts = ["我","喜欢","你","❤️","!"]

while let word = textPrinter(with: texts, index: index) {
print(word)
index+=1
}

输出结果:

1
2
3
4
5

喜欢

❤️
!

通过上述代码,我们可以得到一些结论:

  • textPrinter函数的主要逻辑:判断了index范围的合法性,并返回位置为index的字符串。
  • index的初始化、递增等逻辑写在外部,没有与函数融为一体,容易出错,不易于复用。

对普通函数进行了一番无情的批判,下面是闭包实现版本,可细细品味与普通函数的不同之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Foundation

func makeTextPrinter(with texts: [String]) -> (() -> String?) {
var index = 0
return { () -> String? in
if index < texts.count {
let oldIndex = index
index = index + 1
return texts[oldIndex]
}
return nil
}
}

let getNextLoveWord = makeTextPrinter(with: ["我","喜欢","你","❤️","!"])

while let word = getNextLoveWord() {
print(word)
}

通过上述闭包版本代码,我们也可以得出一些结论:

  • makeTextPrinter返回的匿名函数捕获了一个变量index,延长了这个变量的生命周期,它也是我们一直强调的关联的环境
  • index如成员变量一样与函数绑定在一起,形成一个闭包。index的生命周期和闭包的生命周期一致。
  • 在闭包生命周期内,闭包内部可以持续修改被捕获的变量index,所以闭包看起来像一个对象。
  • index的初始化、递增等逻辑在函数内部实现,这样更加清晰,也更容易被复用,具体可以与普通函数版本对比。

参考资料

闭包 | 维基百科

柯里化 | 维基百科