闭包,就是能够读取其他函数内部变量的函数。Swift 中的闭包与 C 和 OC 中的 block 以及其他编程语言中的匿名函数类似。Swift 中的闭包有以下几种形式:
- 全局函数:有名称,但是不能捕获任何值的闭包
- 嵌套函数:有名称,也能捕获封闭函数内的值的闭包
- 闭包表达式:没有名称,用轻量级语法编写,可以从周围的上下文中捕获值的闭包
一、闭包表达式
闭包表达式是一种利用简介语法构建内联闭包的方式。在 Swift 中,可以通过关键字 func
来定义一个函数,也可以通过闭包表达式定义一个函数。
{
(参数列表) -> 返回值类型 in
函数体代码
}
例如定义一个函数,传入两个参数求和:
func sum(_ v1: Int, _ v2: Int) -> Int {v1 + v2}
上面我们通过关键字 func
来定义的函数,我们也可以通过闭包表达式来定义:
var fun = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
fun(10, 20)
闭包表达式的简写
闭包表达式还提供了一些语法优化,使得撰写闭包变得简单明了。例如从上下文中推断参数和返回值类型、单闭包表达式的隐式返回、速记参数名称、尾随闭包语法等。下面我们用一个例子的几种简写方式来帮助理解。
我们定义一个函数,然后将上文中求和的闭包表达式作为函数的参数,来求出函数另外两个参数的和。执行函数就能计算出和:
var fun = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
exec(v1: 20, v2: 30, fn: fun) // 50
在调用求和函数exec
时,我们通过简写,可以省略函数外部声明的闭包表达式fun
,将闭包表达式直接写在函数内部:
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}) // 30
由于闭包表达式能够从上下文中推断参数和返回值类型,那么我们还可以简写成:
exec(v1: 10, v2: 20, fn: {
v1, v2 in return v1 + v2
}) // 30
我们甚至连return
都可以省略:
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 + v2
}) // 30
由于我们定义的函数只是简单的将两个参数进行相加,那么就可以用$
符号来代替参数名:
exec(v1: 10, v2: 20, fn: { $0 + $1 }) // 30
甚至可以不用任何参数,仅用一个+
符号来表示求和:
exec(v1: 10, v2: 20, fn: + ) // 30
尾随闭包
尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
上文的exec
函数在定义时就是将一个闭包表达式作为最后一个参数,我们在调用exec
函数时都是将闭包表达式放在函数的()
内来调用。如果我们采用尾随闭包,那么调用exec
函数就可以将闭包表达式放在函数的()
的外面:
exec(v1: 10, v2: 20) {
v1, v2 in v1 + v2
}
如果将一个很长的闭包表达式作为函数的最后一个实参,那么使用尾随闭包就可以增强函数的可读性。
闭包表达式的应用
如果我们在日常开发中相对一个数组进行排序,可以通过系统提供的sort
函数或者sorted
函数直接实现。例如:
var arr = [10, 23, 1, 45, 39]
arr.sort()
print(arr) // [1, 10, 23, 39, 45]
可以看出系统提供的sort
函数是升序排序,如果我们想降序排序呢?Swift 中系统就提供了一个函数,来让我们自定义排序是降序还是升序。
func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool)
这个函数需要传入两个参数,会返回Bool
类型的返回值。
var arr = [10, 23, 1, 45, 39]
/* v1 > v2
* 返回true,v1排在v2前面
* 返回false,v1排在v2后面
*/
func cmp(v1: Int, v2: Int) -> Bool {
return v1 > v2
}
arr.sort(by: cmp)
print(arr) // [45, 39, 23, 10, 1]
二、闭包
一个函数和它所捕获的变量或者常量环境组合起来,我们称之为闭包。闭包一般指的是在函数内部的函数,它捕获的是外层函数的局部变量或者常量。
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus (_ i: Int) -> Int {
num += i
return num
}
return plus
}
我们定义一个函数Fn
,并将函数Fn
作为另外一个函数getFn
的返回值。那么例子中的plus
和num
就构成了闭包。
在内存空间中,闭包是保存在堆空间的。我们可以把闭包当做一个类的实例对象,捕获的局部变量或常量就是实例对象的属性,组成闭包的函数就是这个类内部定义的方法。
我们来看一下下面的代码,调用上面例子中的getFn
函数:
var fn1 = getFn()
print(fn1(1)) // 1
print(fn1(2)) // 3
print(fn1(3)) // 6
var fn2 = getFn()
print(fn2(5)) // 5
print(fn2(6)) // 11
print(fn2(7)) // 18
由于闭包是保存在堆空间中的,所以当我们调用getFn
函数时,系统就会为函数内部的闭包在堆空间中分配一块内存以用来保存捕获的局部变量num
。所以我们在执行fn1
和fn2
时才能完成打印。而在执行var fn1 = getFn()
和var fn2 = getFn()
时,系统会分配一块新的内存空间。
三、自动闭包
我们首先来看下面的例子,一个函数传入两个参数,如果第一个参数大于 0 就返回第一个参数,否则就返回第二个参数:
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 12) // 10
getFirstPositive(0, 20) // 20
getFirstPositive(-1, 14) // 14
代码很简单,也实现了我们上面的需求。但是这段代码实际是有问题的,原则上来将如果我们传入的第一个参数大于 0 ,那么就不要再继续判断第二个数了,但是实际上编译器会继续判断第二个参数。我们声明一个函数来验证一下:
func getNum() -> Int {
let a = 10
let b = 20
return a + b
}
getFirstPositive(20, getNum())
当我们将getNum()
函数作为第二个参数传入函数时,在getNum()
函数内部打上断点,运行发现进入断点了。
此时我们就可以把函数的第二个参数v2
设置为一个函数,这样可以避免编译器进行的多余的判断:
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
我们再执行就会发现如果第一个参数大于 0 ,就不会再进行第二个参数的判断。我们将之前的Int
类型的参数v2
改成了函数类型,这样就可以让参数v2
延迟加载。如果我们在参数v2
的函数中的代码很复杂或者开销很大,延迟加载就能帮我们提高效率。
当然,Swift 中提供了自动闭包这一技术,进行了编译器优化,就是使用@autoclosure
关键字修饰参数v2
,提高了代码的可读性:
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
我们在调用函数时,就可以直接调用。这样编译器也不会再去判断第二个参数:
getFirstPositive(1, 14) // 1
要注意的是,@autoclosure
关键字只能修饰() -> T
格式的参数,也就是说修饰的函数必须是无参带返回值的函数。当然@autoclosure
关键字并非只支持最后一个参数,如果上面例子中getFirstPositive
函数在v2
参数后面还有参数,@autoclosure
关键字也可以修饰v2
参数。我们在之前的文章中介绍的空合并运算符 ??
也是使用了@autoclosure
技术。