swift进阶十三:闭包
在 swift进阶八:闭包 & Runtime & Any等类型 中,我们介绍了闭包
捕获变量
特性。本节,我们继续了解闭包
:
- 什么是
闭包
-
闭包
的结构
-
逃逸闭包
与非逃逸闭包
自动闭包
- 函数作形参
1. 什么是闭包
来自维基百科的解释:
-
在计算机科学中,
闭包
(Closure),又称词法闭包
(Lexical Closure)或函数闭包
(function closures),是在支持头等函数
的编程语言
中实现词法绑定
的一种技术
。 -
闭包
在实现上是一个结构体
,它存储
了一个函数
(通常是其入口地址
)和一个关联
的环境
(相当于一个符号查找表
)
1.1 全局函数
一种特殊
的闭包
-
无捕获变量
的全局函数
,也属于闭包
func test(){
print(a)
}
1.2 内嵌函数
也是闭包
,会捕获外部变量
:
(incrementer
是内嵌函数
,也是一个闭包
,捕获了上层函数
的runningTotal
)
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 内嵌函数(也是一个闭包,捕获了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
print(makeIncrementer()) // (()-> Int)匿名函数
print(makeIncrementer()()) // Int
打印值:
image.png1.3 闭包表达式
闭包
是一个匿名函数
- 所有
代码
都在花括号内
参数
和返回类型
在in
关键字之前in
之后是主体内容
:
{ (参数)-> 返回类型 in
// do something
}
- swift的
闭包
可以当作let 常量
、var 变量
,也可以当作参数
传递。
// 常量
let closure1: (Int) -> Int
// 变量
var closure2 : (Int) -> Int = { (age: Int) in
return age
}
// 参数传递
func test(params: ()->Int) {
print(params())
}
var age = 10
// 执行(尾随闭包)
test { () -> Int in
age += 1
return age
}
-
swift闭包
支持可选类型
:在()外
使用?
声明
// 可选值:在()外使用?声明
var closure: ((Int) -> Int)?
1.4 尾随闭包
当闭包表达式
作为函数
的最后一个参数
时,可通过尾随闭包
的书写方式
来提高
代码的可读性
:
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item: Int, _ item3: Int) -> Bool) -> Bool{
return by(a, b, c)
}
// 常规写法
test(10, 20, 30, by: { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
return itme1 + itme2 < itme3
})
// 快捷写法(小括号提到最后一个参数前)
test(10, 20, 30) { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
return itme1 + itme2 < itme3
}
// 最简洁写法 (入参直接使用$0 $1 $2代替,单行代码可省略return)
test(10, 20, 30) { $0 + $1 < $2 }
可以看到,最简洁
的写法
看上去非常舒服
,语义
表达清晰
闭包表达式
是swift
的语法
。使用闭包表达式
可以更简洁
的传递信息
。好处多多:
- 利用上下文
推断参数
和返回类型
单表达式
可以隐式返回
,省略return
关键字参数名称
可以直接使用简写(如$0
,$1
,元组的$0.0
)尾随闭包
可以更简洁的表达
var array = [1, 2, 3]
// 1. 常规写法
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
// 2. 省略类型 (根据上下文,可自动推断参数类型)
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
// 3. 省略返回类型 (根据上下文,可自动推断返回类型)
array.sort(by: {(item1, item2) in return item1 < item2 })
// 4. 省略return (单行表达式,可省略return)
array.sort{(item1, item2) in item1 < item2 }
// 5. 参数简写 (使用$0 $1,按位置顺序获取参数)
array.sort{ return $0 < $1 }
// 6. 省略return (单行表达式,可省略return)
array.sort{ $0 < $1 }
// 7. 使用高阶函数,传递排序规则
array.sort(by: <)
2. 闭包的结构
- 我们使用
变量
记录闭包
,发现内部属性
也被记录了。
(下面runningTotal
被记录,多次打印
时,runningTotal
结果不一样。
如果直接调用
闭包,会发现runningTotal
结果一样)
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 内嵌函数(也是一个闭包,捕获了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 函数变量 (存储格式是怎样?)
var makeInc = makeIncrementer()
print(makeInc()) // 打印: 11
print(makeInc()) // 打印: 12
print(makeInc()) // 打印: 13
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
Q: 把
函数
赋值给变量
,变量存储
的是函数地址
还是什么
?
- 在
SIL
中看不到makeInc
的结构:
image.png
- 我们再往后一层,直接查看
ir
代码:
拓展
image.pngIR
与IR语法
:
Swift编译流程
- 将
swift源码
编译为AST语法树
swiftc -dump-ast HTPerson.swift > ast.swift
- 生成
SIL
源码
swiftc -emit-sil HTPerson.swift > ./HTPerson.sil
- 生成
IR
中间代码
swiftc -emit-ir HTPerson.swift > ir.swift
- 输出
.o
机器文件
swiftc -emit-object HTPerson.swift
ir语法 👉 官方文档
- 数组
[<elementnumber> x <elementtype>] // [数组梳理 x 数组类型] // example alloca[24 x i8],align8 // 24个i8都是0
- 结构体
%swift.refcounted = type { %swift.type*, i64 } // { 指针类型,类型} // example %T = type {<type list>} // 和C语言结构体类似
- 指针类型
<type> * // example i64* // 64位的整形
getelementptr
指针别名
可通过getelementptr
读取数组
和结构体
的成员:<result> = getelementptr <ty>, <ty>* <ptrval> {, [inrange] <ty> <id x>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
getelementptr
的读取案例
:
( 创建一个c
语言工程,main.c
中加入测试代码
)// example struct munger_struct { int f1; int f2; }; void munge(struct munger_struct *p) { p[0].f1 = p[1].f1 + p[2].f2; // 假设P是有3个元素的数组。就可以直接通过下标读取 } struct munger_struct array[3]; int main(int argc, const char * argv[]) { munge(array); //调用 return 0; }
- LLVM中,
C语言
需要使用Clang
输出IR
文件:
clang -S -fobjc-arc -emit-llvm main.c
image.png
- 熟悉了
IR语法
后,回到这个代码:
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 函数变量 (存储格式是怎样?)
var makeInc = makeIncrementer()
-
输出
image.pngIR文件
,分析结构:
-
按照
IR
分析的代码,我们自定义结构
,读取函数指针地址
和内部属性值
:
由于
无法
直接读取()-> Int
函数的地址
,所以利用结构体
地址就是首元素地址
的特性,将函数
设为结构体第一个属性
。通过读取结构体
指针地址
,获取到函数地址
struct FunctionData<T> {
var pointer: UnsafeRawPointer
var captureValue: UnsafePointer<T>
}
struct Refcounted {
var pointer: UnsafeRawPointer
var refCount: Int64
}
struct Type {
var type: Int64
}
struct Box<T> {
var refcounted: Refcounted
var value: T // 8字节类型,可由外部动态传入
}
struct BoxMetadata<T> {
var refcounted: UnsafePointer<Refcounted>
var undefA: UnsafeRawPointer
var type: Type
var undefB: Int32
var undefC: UnsafeRawPointer
}
// 由于无法直接读取`()-> Int`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性
struct VoidIntFunc {
var f: ()->Int
}
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 内嵌函数(也是一个闭包,捕获了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 使用struct包装的函数
var makeInc = VoidIntFunc(f: makeIncrementer())
// 读取指针
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)
// 初始化
ptr.initialize(to: makeInc)
// 将指针绑定为FunctionData<Box<Int>>类型,返回指针
let context = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee }
// 打印指针值 与 内部属性值
print(context.pointer) // 打印 0x00000001000054b0
print(context.captureValue.pointee.value) // 打印 10
image.png
image.png
验证
打印的函数地址
是否是makeIncrementer
函数:
打开终端,输入命令:nm -p 编译后的machO文件地址 | grep 函数地址
// 例如: nm -p /Users/asetku/Library/Developer/Xcode/DerivedData/Demo-bhpsxmnrzusvmeaotyclgmelcxpp/Build/Products/Debug/Demo | grep 00000001000054b0
- 可以看到,该地址正是
函数
的地址
image.png- 使用
xcrun swift-demangle XXXX
命令,还原
函数名
image.png
所以:
函数
事故引用类型
,被赋值的变量
记录了函数地址
。
函数内
的变量
,会alloc
开辟空间,调用前后,会retain
和release
管理引用计数
拓展: 如果
函数
内部多个属性
,结构
是怎样呢?func makeIncrementer() -> (()-> Int){ var aa = 10 var bb = 20 // 内嵌函数(也是一个闭包,捕获了runningTotal) func incrementer() -> Int { aa += 6 bb += 9 return bb } return incrementer } var makeInc = makeIncrementer()
基础结构没有变化
image.png相比起
image.png单变量
,多了一个临时结构
,把两个变量
分别用指针
记录:
image.pngstruct FunctionData<T> { var pointer: UnsafeRawPointer var captureValue: UnsafePointer<T> } struct Refcounted { var pointer: UnsafeRawPointer var refCount: Int64 } struct Type { var type: Int64 } struct Box<T> { var refcounted: Refcounted var value: T // 8字节类型,可由外部动态传入 } // 多了一个Box2结构,每个变量都是`Box`结构的对象 struct Box2<T> { var refcounted: Refcounted var value1: UnsafePointer<Box<T>> var value2: UnsafePointer<Box<T>> } struct BoxMetadata<T> { var refcounted: UnsafePointer<Refcounted> var undefA: UnsafeRawPointer var type: Type var undefB: Int32 var undefC: UnsafeRawPointer } // 由于无法直接读取`()-> Int`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性 struct VoidIntFunc { var f: ()->Int } func makeIncrementer() -> (()-> Int){ var aa = 10 var bb = 20 // 内嵌函数(也是一个闭包,捕获了runningTotal) func incrementer() -> Int { aa += 6 bb += 9 return bb } return incrementer } // 使用struct包装的函数 var makeInc = VoidIntFunc(f: makeIncrementer()) // 读取指针 let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1) // 初始化 ptr.initialize(to: makeInc) // 将指针绑定为FunctionData<Box<Int>>类型,返回指针 let context = ptr.withMemoryRebound(to: FunctionData<Box2<Int>>.self, capacity: 1) { $0.pointee } // 打印指针值 与 两个内部属性值 print(context.pointer) // 打印: 0x00000001000050c0 print(context.captureValue.pointee.value1.pointee.value) // 打印:10 print(context.captureValue.pointee.value2.pointee.value) // 打印: 20
- 在
MachO文件
中校验函数地址
的值
,确定是makeIncrementer
函数:
image.png
3. 逃逸闭包与非逃逸闭包
-
逃逸闭包:
当闭包
作为一个实际参数
传递给一个函数
,且在函数返回
之后调用
,我们就说这个闭包逃逸
了。
(逃逸闭包
作为函数形参
时,需要使用@escaping
声明,生命周期
比函数
要长
。如:被外部变量持有
或异步延时调用
) -
非逃逸闭包:
系统默认
闭包参数是@nonescaping
声明, 是非逃逸闭包
,生命周期
与被调用
的函数
保持一致
。
3.1 逃逸闭包
-
闭包
被函数外变量
持有,需要@escaping
声明为逃逸闭包
-
闭包
被异步线程
延时调用,需要@escaping
声明为逃逸闭包
class HTPerson {
var completion: ((Int)->Void)?
func test1(handler: @escaping (Int)->Void) {
// 1. 外部变量持有handler,handler的生命周期逃到了`makeIncrementer`函数外
self.completion = handler
}
func test2(handler: @escaping (Int)->Void) {
// 2. 异步线程延时调用handler,handler的生命周期逃到了`makeIncrementer`函数外
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
handler(10)
}
}
}
3.2 非逃逸闭包
系统默认
闭包为非逃逸闭包
,编译期
自动加上@noescape
声明,生命周期
与函数一致
。
func test(closure: (()->())) {}
-
SIL编译
可以看到默认使用@noescape
声明闭包:
image.png
4. 自动闭包
-
自动闭包
:自动识别
闭包返回值
,可直接接收
返回值类型数据
。
需要用@autoclosure
声明,不接收
任何参数
,返回值
是当前内部表达式
的值
(如()->String
)
// 自动闭包`@autoclosure`声明
func testPrint(_ message: @autoclosure ()->String) {
print(message())
}
func doSomeThing() -> String {
return "吃了吗?"
}
// 入参传`函数`
testPrint(doSomeThing())
// 入参传`字符串`
testPrint("干啥呢")
- 可以看到,使用
自动闭包
时,参数可以是函数
,也可以是闭包返回类型
(字符串).
自动闭包
可以兼容函数入参
类型(函数
/函数返参类型
)
5. 函数作形参
-
当
函数
作为参数
进行传递
时,可节省计算量
,在合适时期
再执行
。 -
普通函数:
doSomeThing
是一个耗时操作
,计算结果
传给testPrint
时,testPrint
由于条件不满足
,压根没用到
这个耗时操作
的结果
。
func testPrint(_ condition: Bool, _ message: String) {
if condition {
print("错误信息: \(message)")
}
print("结束")
}
func doSomeThing() -> String {
print("执行了")
// 耗时操作,从0到1000拼接成字符串
return (0...1000).reduce("") { $0 + " \($1)"}
}
testPrint(false, doSomeThing())
image.png
- 使用函数作为入参:
入参
直接传入函数
,未满足条件
时,不
会执行函数
,避开了耗时操作
func testPrint(_ condition: Bool, _ message: ()->String) {
if condition {
print("错误信息: \(message())")
}
print("结束")
}
func doSomeThing() -> String {
print("执行了")
// 耗时操作,从0到1000拼接成字符串
return (0...1000).reduce("") { $0 + " \($1)"}
}
testPrint(false, doSomeThing())
image.png
注意
- 上述只是演示
函数
作为入参
,没有
或单次
调用时,可延时
在合适时期调用
,避免资源提前计算
。- 但如果该
资源
会被多次调用
,还是提前计算资源
才节省资源
。
至此,我们对闭包
有了完整认识
。下一节介绍Optional
& Equatable
& 访问控制