闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块 (blocks) 以及其他一些编程语言中的匿名函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。
闭包表达式
在 Swift 中可以通过 func 关键字 定义一个函数:
func sum(_ v1: Int, _ v2: Int) -> Int {
return v1 + v2
}
sum 函数调用:
sum(10, 20) // 30
也可以用过闭包表达式定义一个函数
var fn = { (v1: Int, v2: Int) -> Int in
return v1 + v2
}
闭包表达式调用:
fn(10, 20) // 30
在 Swift 中函数是一类公民,可以作为参数、返回值,跟 Int、Array、Class 等没有区别:
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
观察 exec 函数,这是有三个参数的无返回值函数:
- 第1个参数:v1,类型为
Int
- 第2个参数:v2,类型为
Int
- 第1个参数:fn,类型为
(Int, Int) -> Int)
- 无返回值
调用 exec 函数:
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
// 注意:这里的 v1 和 v2 是 fn 闭包表达式内的参数,跟 exec 函数的 v1、v2 没有任何关系
return v1 + v2
})
闭包的精简写法
上面 exec 的调用虽然很容易理解,但看上去有些冗长:参数类型满天飞、fn
的参数 v1
、v2
跟exec
本身的前两个参数容易混淆。
强大的 Swift 编译器允许我们对其做一些精简,下面一步步做介绍:
-
由于在定义
exec
的时候已经明确了fn
的两个参数类型Int
和返回类型Int
,所以可以做如下简化:// 只需要使用 in 关键字 将参数和函数体做区隔即可,省略了v1、v2的类型以及返回值 exec(v1: 10, v2: 20, fn: { v1, v2 in return v1 + v2 })
-
就跟函数一样,闭包函数体的单行表达式可以省略
return
// 省略了函数体的 return 关键字 exec(v1: 10, v2: 20, fn: { v1, v2 in v1 + v2 })
-
Swift 中可以使用
$0
和$1
来分别表示第0个参数和第1个参数
exec(v1: 10, v2: 20, fn: {
$0 + $1
})
- 甚至于你可以省略
$0
和$1
(这种方式过分了,不推荐😓)
// 直接使用一个 + ,表示第0个参数和第1个参数直接是加号运算符
exec(v1: 10, v2: 20, fn: +)
尾随闭包
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。
尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
在使用尾随闭包时,你不用写出它的参数标签。
还用之前的 exec 函数举例
- 不使用尾随闭包:
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
return v1 + v2
})
- 使用尾随闭包:
exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in return v1 + v2 }
尾随闭包不仅仅省略了 fn
形参,而且将{函数体}
挪到了()
外面让整个函数调用更加的直观易读。同样可以对其进行精简:
swift // 省略闭包参数类型和返回值 exec(v1: 10, v2: 20) { v1, v2 in // 省略 return v1 + v2 }
然后:
swift exec(v1: 10, v2: 20) { // 直接使用 $0、$1 表示第0和第1个参数 $0 + $1 }
这个表达式就非常的简洁优雅了!👍🏻 注意,下面的表达式是不允许的:
swift // 尾随闭包不允许省略$0、$1 exec(v1: 10, v2: 20) { + }
闭包的值捕获
闭包可以理解为函数以及其捕获的上下文中的变量或常量的总和
看下面这个函数:
func getFn() -> (Int) -> Int {
var num = 0
func plus (_ i: Int) -> Int {
num = num + i
return num
}
return plus
}
getFn
函数没有参数,返回值为(Int) -> Int
(一个参数为Int
返回值为Int
的函数)。
getFn
函数体内定义了一个Int
类型的变量num
,又定义了一个plus
函数,并将其作为getFn
函数的返回值返回。
plus
函数对num
变量进行了捕获,构成了闭包。
思考如下代码的输出:
var f = getFn()
print(f(1))
print(f(2))
print(f(3))
print(f(4))
结果是哪一组?
1 1
2 or 3
3 6
4 10
正如前面提到的函数以及其捕获的上下文中的变量或常量的总和,当调用getFn()
时,返回的不仅仅是plus
函数同时也包括num
变量组成的闭包整体!理解这个概念非常重要,因此getFn()
返回的其实就是下面的代码片段:
var num = 0
func plus (_ i: Int) -> Int {
num = num + i
return num
}
因此f(1)``f(2)``f(3)``f(4)
访问的是同一个num
,或者说同一块变量内存
num 初始值为 0
f(1)就等价于 num = num + 1
f(2)就等价于 num = num + 2
f(3)就等价于 num = num + 3
f(4)就等价于 num = num + 4
所以结果为:
1
3
6
10
再思考这种情况:
var f1 = getFn()
print(f1(1)) // 1
print(f1(2)) // 3
print(f1(3)) // 6
var f2 = getFn()
print(f2(1)) // 1
print(f2(2)) // 3
print(f2(3)) // 6
很显然每创建一个getFn
函数引用(没错,函数和闭包都是引用类型),Swift 都会为所捕获的num
申请一份新的堆空间内存,来保证所有的f1
访问的都是同一块内存地址,所有的f2
访问的也都是同一块内存地址,但f1
和f2
访问的num
堆地址不是同一块!
自动闭包
观察下面这个函数:
// 如果第1个参数大于0则返回之,否则返回第2个参数
func getFirstPositiveNumber(n1: Int, n2: Int) -> Int {
return n1 > 0 ? n1 : n2
}
调用getFirstPositiveNumber
:
func getDoubleOfNumber(_ v: Int) -> Int {
return v * 2
}
getFirstPositiveNumber(n1: 10, n2: 20) // 10
getFirstPositiveNumber(n1: -10, n2: 20) // 20
上述函数看似很简单,但有一个隐患可以优化:
如果n1 > 0
,那么n2
是什么根本不重要了,可是编译器还是需要花费开销去"关心"n2
。你可能会不以为然,心理嘀咕『不就一个Int
,至于么?"』
那下面这个例子呢:
func getNumber() -> Double {
return Double.pi * 10.0
}
getFirstPositiveNumber(n1: 10.0, n2: getNumber()) // 10.0
既然n1
已经>0
了,我们为何还要去调用getDoubleOfNumber
来计算n2
呢?
如果getDoubleOfNumber
函数 计算很复杂 、 需要去读取本地数据 甚至 需要联网抓取数据 呢?这种浪费就不能不以为然了吧。
那怎么解决呢?当时是使用闭包:
func getFirstPositiveNumber(n1: Double, n2: () -> Double) -> Double {
return n1 > 0 ? n1 : n2()
}
将n2
的类型从Double
改为() -> Double
,调用时:
getFirstPositiveNumber(n1: 10.0, n2: {
Double.pi * 10.00
})
或者使用尾随闭包:
getFirstPositiveNumber(n1: 10) {
Double.pi * 10.00
}
当n1 > 0
时,闭包的函数体Double.pi * 10.00
根本不用执行!完美!
可是每次调用getFirstPositiveNumber
都要写闭包会很繁琐,因此Swift标准库提供了自动闭包的语法糖来解决这个问题,getFirstPositiveNumber
函数只需要像下面这么写:
func getFirstPositiveNumber1(n1: Double, n2: @autoclosure () -> Double) -> Double {
return n1 > 0 ? n1 : n2()
}
getFirstPositiveNumber1(n1: 10, n2: Double.pi * 2)
调用时n2
会自动写成闭包的形式!
逃逸闭包
func fn1(_ closure: (Int) -> Int) {
print(closure(10))
}
fn1
函数只有一个闭包参数closure
,且closure
在fn1
函数体内部直接调用,这时候我们称closure
为非逃逸闭包。
如果像下面这么写编译器就会报错:
var c: ((Int) -> Int)?
func fn2(_ closure: (Int) -> Int) {
c = closure
}
closure
在fn2
作用域外调用,即成为逃逸闭包,可以使用@escaping
关键词消除编译器报错:
func fn2(_ closure: @escaping (Int) -> Int) {
c = closure
}
网友评论