什么是闭包?闭包表达式又是什么?
一、闭包表达式(Closure Expression)
在Swift中,可以通过func
定义一个函数,也可以通过闭包表达式定义一个函数。
1.1. 闭包表达式的格式
代码语言:javascript复制{
(参数列表) -> 返回值类型 in
函数体代码
}
1.2. 闭包表达式和函数的比较
定义一个普通的函数:
代码语言:javascript复制func sum(_ v1: Int, _ v2: Int) -> Int { v2 v2 }
let result = sum(10, 20)
print(result)
// 输出:30
定义闭包:
代码语言:javascript复制var sum = {
(v1: Int, v2: Int) -> Int in
return v1 v2
}
let result = sum(10, 20)
print(result)
// 输出:30
1.3. 闭包表达式的简写
定义函数:
代码语言:javascript复制func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
要想使用exec
函数,则必须传入两个Int
类型的参数和一个返回Int
类型的函数,然后exec
内部执行了传入的函数。
func sum(_ a: Int, _ b: Int) -> Int {
return a b
}
exec(v1: 10, v2: 20, fn: sum)
// 输出:30
按照以往的知识,我们需要定义一个函数,然后把函数传给exec
就行了。其实我们也可以使用闭包表达式。
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 v2
})
// 输出:30
上面的闭包表达式还可以简写:
1.3.1. 简写一
- 省略参数类型和返回值;
- 编译器会自动推断闭包表达式中参数类型和返回值类型。
exec(v1: 10, v2: 20, fn: {
v1, v2 in return v1 v2
})
// 输出:30
1.3.2. 简写二
如果函数的返回值是一个单一表达式,可以省略return
。
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 v2
})
// 输出:30
1.3.3. 简写三
如果闭包表达式不想写参数,可以使用美元符$序号
代替,序号从0开始,代表参数位置。
exec(v1: 10, v2: 20, fn: { $0 $1 })
// 输出:30
1.3.4. 简写四(不建议)
简单的闭包表达式还可以直接使用运算符代替。
代码语言:javascript复制exec(v1: 10, v2: 20, fn: )
// 输出:30
二、尾随闭包
2.1. 特点一(最后一个实参)
如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。
尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。
以调用上面的exec
函数为例:
exec(v1: 10, v2: 20) {
$0 $1
}
// 输出:30
2.2. 特点二(唯一实参)
如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号。
定义函数:
代码语言:javascript复制func exec(fn: (Int, Int) -> Int) {
print(fn(10, 20))
}
调用方式:
代码语言:javascript复制// 方式一:
exec(fn: { $0 $1 })
// 方式二:
exec() { $0 $1 }
// 方式三:
exec { $0 $1 }
/*
输出:
30
30
30
*/
三、闭包(Closure)
闭包和闭包表达式严格意义上来讲并不是同一个概念。
一个函数和它所捕获的变量/常量环境组合起来,称为闭包。
- 一般指定义在函数内部的函数;
- 一般它捕获的是外层函数的局部变量/常量。
示例代码:
代码语言:javascript复制typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num = 1
return num
}
return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
/*
输出:
1
3
6
10
*/
为什么var num = 0
作为局部变量还能一直累加?不是应该在函数执行完成后就被释放了么?我们通过汇编一探究竟。
3.1. 汇编分析闭包
3.1.1. 如果内部函数没有捕获外部变量:
通过分析可以看到,函数返回的是一个地址,也就是变量fn
里面存放的是函数地址。
3.1.2. 如果内部函数捕获外部变量:
汇编代码就变得复杂一点了,并且出现了swift_allocObject
关键字,也就意味着在堆空间申请了一块内存,内存存放的是num
的值。每次调用fn
,访问的num
都是同一块内存地址,所以才会出现局部变量也能一直累加的效果。
3.1.3. 证明swift_allocObject
存放的是num
:
第一步:源代码断点:
第二步:查看swift_allocObject
返回的地址:
第三步:查看rax
地址存放的初始化值:
第四步:执行fn(1)
后:
第五步:执行fn(2)
后:
结论: 内部函数一旦捕获了外部的局部变量,要想持续使用这个变量,就需要延迟变量的生命周期,所以在堆空间分配一块内存来存放局部变量的值。
思考:为什么可以访问同一块内存空间?
var fn = getFn()
fn占用16个字节,前8个字节存放返回的函数地址(plus的封装
),后8个字节存放堆空间(num
)的地址。如果var fn2 = getFn()
,fn1
和fn2
前8个字节可能相同,不同的是后面的8个字节。
3.2. 闭包和类的比较
可以把闭包想象成是一个类的实例对象。
- 内存在堆空间;
- 捕获的局部变量/常量就是对象的成员(存储属性);
- 组成闭包的函数就是类内部定义的方法。
class Closure {
var num = 0
func plus(_ i: Int) -> Int {
num = i
return num
}
}
var cs = Closure()
print(cs.plus(1))
print(cs.plus(2))
print(cs.plus(3))
print(cs.plus(4))
/*
输出:
1
3
6
10
*/
四、自动闭包
示例代码:
代码语言:javascript复制// 如果第一个数大于0,返回第一个数,否则返回第二个数
func getFirst(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirst(10, 20) // 10
getFirst(-2, 20) // 20
getFirst(0, -4) // -4
把上面的代码修改如下:
代码语言:javascript复制func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a b
}
let result1 = getFirst(10, getNumber())
print(result1)
/*
输出:
getNumber
10
*/
let result2 = getFirst(-1, getNumber())
print(result2)
/*
输出:
getNumber
20
*/
分析:不管第一个数是否大于0,都会执行第二个参数传入的函数,这样整体有点浪费(性能/空间)。我们可以尝试把函数第二个入参类型修改为函数类型。
优化代码:
代码语言:javascript复制typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a b
}
getFirst(10, getNumber)
/*
输出:
getFirst
*/
getFirst(-1, getNumber)
/*
输出:
getFirst
getNumber
*/
结果:只有需要的时候才会执行对应的代码。
但是,如果这样修改后,每次都需要传入一个函数会有点麻烦。Swift提供了自动闭包功能,可以把普通变量自动包裹成闭包,这样就能满足上面代码的所有的功能了。
关键字: @autoclosure
用法:在函数前面加上@autoclosure
关键字即可。
自动闭包代码:
代码语言:javascript复制typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: @autoclosure VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
getFirst(10, 20) // 10
getFirst(-1, 10) // 10
自动闭包特点:
@autoclosure
会将普通参数(例如,20
)封装成闭包{ 参数 }
(例如,{ 20 }
);@autoclosure
只支持() -> T
(无参有返回值)格式的参数;@autoclosure
并非只支持最后一个参数,和位置没有任何关系;- 有
@autoclosure
、无@autoclosure
,构成函数重载; - 为了避免与期望冲突,使用了有
@autoclosure
的地方最好明确注释清楚:这个值会被延迟执行(有可能不执行)。
延伸: 空合并运算符??
使用了@autoclosure
技术。
public func ?? <t>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
五、应用
通过数组的排序看下闭包表达式是如何使用的。
定义函数:
代码语言:javascript复制var arr = [20, 52, 19, 3, 80, 72]
3.1. 系统排序
在Swift中,Array
为开发者提供了sort()
排序函数,开发者可以直接使用。
arr.sort()
print(arr)
// 输出:[3, 19, 20, 52, 72, 80]
3.2. 自定义排序
sort()
是升序的,如果要降序呢?我们可以使用另外一个函数进行自定义排序。
Array
提供的函数:
func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
可以看到,该函数让传入一个闭包表达式。使用规则如下:
- 返回true:第一个元素排在第二个元素前面;
- 返回false:第一个元素排在第二个元素后面。
调用方式一(普通函数):
代码语言:javascript复制func compare(i1: Int, i2: Int) -> Bool {
return i1 > i2
}
arr.sort(by: compare)
print(arr)
// 输出:[80, 72, 52, 20, 19, 3]
调用方式二(闭包表达式):
代码语言:javascript复制arr.sort(by: {
(i1: Int, i2: Int) -> Bool in
return i1 > i2
})
arr.sort(by: { i1, i2 in return i1 > i2 })
arr.sort(by: { i1, i2 in i1 > i2 })
arr.sort(by: { $0 > $1 })
arr.sort(by: >)
arr.sort() { $0 > $1 }
arr.sort { $0 > $1 }
// 输出:[80, 72, 52, 20, 19, 3]