闭包表达式
- 在 swift 中,可以通过 func 定义一个函数,也可以闭包表达式定义一个函数
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
var fn: (Int, Int) -> Int = { i, j in
return i + j
}
exec(v1: 1, v2: 3, fn: fn)
exec(v1: 1, v2: 3) { (i, j) -> Int in
return i + j
}
exec(v1: 1, v2: 2) { $0 + $1 }
exec(v1: 1, v2: 2, fn: +)
尾随闭包
- 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
- 尾随闭包是一个被书写在函数调用括号外面的闭包表达式
- 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后面写圆括号
exec(v1: 1, v2: 3) { (i, j) -> Int in
return i + j
}
// 下面这个就可以不写括号
// 像 array 里面的 reduce、filter、sort、map 等等
exec { v1 + v2 }
示例: sort 的用法
var a = [1, 3, 2, 8, 7, 5]
var b = a
a.sort(by: <)
b.sort { $0 < $1 }
闭包(和闭包表达式完全是两回事)
- 一个函数和它所捕获的变量/常量组合起来,称为闭包
- 一般指定义在函数内部的函数
- 一般捕获的是外层函数的局部变量/常量
- 这个和 oc 中的 block 就完全不是一个概念了,swift 中如果不捕获外层的局部变量/常量的话,其实不能算是闭包,只能算是一个函数或者方法
swiftstudy`test18():
typealias Fn = (Int) -> Int
func getFn() -> Fn {
// 局部变量
var num = 1
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}// 返回的 plus 和 num 形成了闭包
let fn = getFn()
print(fn(1))
print(fn(1))
print(fn(1))
print(fn(1))
------------------执行结果---------------------
2
3
4
5
------------------汇编分析---------------------
swiftstudy`getFn #1 () in test17():
movl $0x18, %esi // swift_allocObject(24),堆是以16字节为基本单位,所以实际分配的内存为32个字节
movl $0x7, %edx
callq 0x100006c5a ; symbol stub for: swift_allocObject
movq %rax, %rdx
addq $0x10, %rdx
movq %rdx, %rsi
movq $0x1, 0x10(%rax)
callq 0x100006cae ; symbol stub for: swift_retain
callq 0x100006ca8 ; symbol stub for: swift_release
leaq 0x2b58(%rip), %rax ; partial apply forwarder for plus
由上面的汇编就可以看出来,实际上在捕捉外层变量/常量的时候,会分配堆内存。这样的话生命周期就被延长了。所以后面也就有对应的 retain 和 release。这也就可以解释为什么执行的结果是 2345,而不是 2222 了。
我们可以分析一下,为什么说不捕获外层的变量/常量的时候就不能称为闭包
swiftstudy`test18():
typealias Fn = (Int) -> Int
func getFn() -> Fn {
func plus(_ i: Int) -> Int {
return i + 1
}
return plus
}
let fn = getFn()
print(fn(1))
------------------汇编分析---------------------
swiftstudy`getFn #1 () in test18():
pushq %rbp
movq %rsp, %rbp
leaq 0x14f5(%rip), %rax ; plus #1 (Swift.Int) -> Swift.Int in getFn #1 () -> (Swift.Int) -> Swift.Int in swiftstudy.test18() -> () at main.swift:275
xorl %ecx, %ecx
movl %ecx, %edx
popq %rbp
retq
可以看出来,跟普通的函数调用没啥区别,根据没有分配堆空间的过程。所以不能算是闭包。
把闭包想像成一个类的实例对象
- 内存在堆空间
- 捕获的局部变量/常量就是对象的成员
- 组成闭包的函数就是类内部定义的方法
上面的代码可以完全改写成下面的这样的形式
class Closure {
var num = 1
func plus(_ i: Int) -> Int {
num += i
return num
}
}
let c = Closure()
c.plus(1)
c.plus(1)
c.plus(1)
c.plus(1)
------------------执行结果---------------------
2
3
4
5
自动闭包
func getNum() -> Int {
let a = 10
let b = 11
print("getNum")
return a + b
}
@discardableResult
func max(a: Int, b: Int) -> Int {
return a > 0 ? a : b
}
print(max(a: 10, b: getNum()))
------------------执行结果---------------------
getNum
21
可以看出来上面的问题就是不管 a 是不是大于0,那么 getNum() 都是会被调用的。应该如何优化呢?
@discardableResult
func max1(a: Int, b: () -> Int) -> Int {
return a > 0 ? a : b()
}
print(max1(a: 10, b: getNum))
这样的话,b就不会执行了,如果这个 b 的闭包表达式非常简单,这种方式就不是很优雅,所以 swift 给我们提供了一种更优雅的实现方式,就是 @autoclosure
- @autoclosure 会自动将 20 封装成闭包 { 20 }
- @autoclosure 只支持 () -> T 格式的参数
- @autoclosure 并非只支持最后一个参数
- 空合并运算符就是利用了 @autoclosure 技术
- 有 @autoclosure 和无 @autoclosure 构成了重载
- 如果有 @autoclosure 你把 20 写成 { 20 } 他会报错
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> ??
@discardableResult
func max2(a: Int, b: @autoclosure () -> Int) -> Int {
return a > 0 ? a : b()
}
print(max2(a: 10, b: getNum()))
思考
-
局部变量/常量是什么时候被捕获的?
是在 return plus 的时候,所以你只要在 return plus 之前修改了 num 的值,那么捕获的其实就是你修改之后的 num 值,如果你 return { $0 },那么它其实也不会捕获 num 的值。
-
那么如果是捕捉的是全局变量,是闭包还是函数?
是函数。都是全局变量了,内存是在数据段,你还在堆内存开辟是脑子有问题吗?局部变量会没命,所以需要开辟堆空间保命,全局就不凑这个热闹了。
-
执行结果是什么?
var functions: [() -> Int] = []
for i in 1...3 {
functions.append { i }
}
for f in functions {
print(f())
}
123