mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
591 lines
19 KiB
Markdown
591 lines
19 KiB
Markdown
# Swift 闭包研究
|
||
|
||
## 方法占用对象内存吗?
|
||
|
||
实验一:
|
||
|
||
```swift
|
||
class Point {
|
||
var test = true
|
||
var age = 29
|
||
var height = 175
|
||
}
|
||
var p = Point()
|
||
print(Mems.size(ofRef: p)) // 48
|
||
```
|
||
|
||
为什么是48,而不是40?
|
||
|
||
Point 类前16位的前8位表示类信息,后8位表示引用计算信息,2个 Int 占2*8 = 16 Byte,Bool 占用1 Byte。所以实际占用 8 + 8 + 2 * 8 + 1 = 33 Byte。但由于存在内存对齐(内存对齐以8为 base,都是8的整数倍),但 malloc 函数分配的内存都是 16的倍数,所以占用48 Byte。
|
||
|
||
|
||
|
||
Demo:
|
||
|
||
```swift
|
||
class Person {
|
||
var age = 29
|
||
func sayHi () {
|
||
var height = 175
|
||
print("局部变量", Mems.ptr(ofVal: &height))
|
||
print("I am \(age) old")
|
||
}
|
||
}
|
||
func sayOuterHi () {
|
||
print("Hello world")
|
||
}
|
||
|
||
var p = Person()
|
||
p.sayHi()
|
||
sayOuterHi()
|
||
|
||
print("全局变量", Mems.ptr(ofVal: &p))
|
||
print("堆空间", Mems.ptr(ofRef: p))
|
||
```
|
||
|
||
<img src="./../assets/ClassMemoryLayoutExcludeFunction.png" style="zoom:25%">
|
||
|
||
<img src="./../assets/ClassMemoryLayoutExcludeFunction2.png" style="zoom:25%">
|
||
|
||
代码段:Person.sayHi 0x1000034d0
|
||
|
||
代码段:sayOuterHi 0x1000038e0
|
||
|
||
全局变量: 0x000000010000c388
|
||
|
||
堆空间: 0x20c820
|
||
|
||
局部变量(栈): 0x00007ff7bfeff2f8
|
||
|
||
可以看到写在类里面的方法地址和写在 Class 外部的方法地址差不多,都在代码段。
|
||
|
||
结论:方法不占用对象的内存。方法、函数存放于代码段。
|
||
|
||
|
||
|
||
## 闭包
|
||
|
||
### 定义
|
||
|
||
什么是闭包?一个函数和它所捕获的变量/常量环境组合起来,称为闭包。
|
||
|
||
- 一般指定义在函数内部的函数
|
||
- 一般捕获的是外层函数的局部变量、常量
|
||
|
||
|
||
|
||
### 原理窥探
|
||
|
||
Demo
|
||
|
||
```swift
|
||
func exec(a: Int, b: Int, fn: (Int, Int) -> Int) {
|
||
print(fn(a, b))
|
||
}
|
||
// 写法1
|
||
func sum(a: Int, b: Int) -> Int { return a + b }
|
||
exec(a: 1, b: 2, fn: sum)
|
||
|
||
// 写法2:闭包
|
||
exec(a: 1, b: 2, fn: {
|
||
(a: Int, b: Int) -> Int in
|
||
return a + b
|
||
})
|
||
// 写法3:闭包简写
|
||
exec(a: 1, b: 2, fn: {
|
||
a,b in return a + b
|
||
})
|
||
// 写法4:闭包简写
|
||
exec(a: 1, b: 2, fn: {
|
||
a,b in a + b
|
||
})
|
||
// 写法5:闭包简写。用$0、$1来获取参数。
|
||
exec(a: 1, b: 2, fn: { $0 + $1 })
|
||
// 写法5:闭包简写。用 + 来代表操作,让编译器进行推断
|
||
exec(a: 1, b: 2, fn: + )
|
||
```
|
||
|
||
如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
|
||
|
||
上面的写法等价于
|
||
|
||
```swift
|
||
// 写法6:尾随闭包
|
||
exec(a: 1, b: 2) {
|
||
$0 + $1
|
||
}
|
||
```
|
||
|
||
如果闭包表达式是函数的唯一实参,而且使用饿尾随闭包的语法,那就不需要在函数名后面写圆括号
|
||
|
||
```swift
|
||
func exec(fn: (Int, Int) -> Int) {
|
||
print(fn(1, 2))
|
||
}
|
||
|
||
exec(fn: { $0 + $1 }) // 3
|
||
exec() { $0 + $1 } // 3
|
||
exec{ $0 + $1 } // 3
|
||
```
|
||
|
||
来个 Demo 看看系统数组的排序
|
||
|
||
```swift
|
||
var array = [1, 8, 9, 12, 32, 2]
|
||
//array.sort()
|
||
func compare(a: Int, b: Int) -> Bool {
|
||
return a < b
|
||
}
|
||
// 写法1
|
||
//array.sort(by: compare)
|
||
|
||
// 写法2
|
||
// array.sort { $0 < $1 }
|
||
|
||
// 写法3
|
||
//array.sort { a, b in
|
||
// return a < b
|
||
//}
|
||
|
||
// 写法4
|
||
//array.sort(by: {
|
||
// (a: Int, b: Int) -> Bool in
|
||
// return a < b
|
||
//})
|
||
|
||
// 写法5
|
||
//array.sort(by: <)
|
||
|
||
// 写法6
|
||
array.sort() { $0 < $1 }
|
||
|
||
print(array) // [1, 2, 8, 9, 12, 32]
|
||
```
|
||
|
||
|
||
|
||
Demo2
|
||
|
||
闭包的变量捕获
|
||
|
||
```swift
|
||
typealias Fn = (Int) -> Int
|
||
func getFn() -> Fn {
|
||
var num = 0
|
||
func plus(_ i: Int) -> Int {
|
||
return i
|
||
}
|
||
return plus
|
||
}
|
||
var fn = getFn()
|
||
print(fn(1)) // 1
|
||
print(fn(2)) // 2
|
||
print(fn(3)) // 3
|
||
```
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo1.png" style="zoom:25%">
|
||
|
||
|
||
|
||
简单修改下代码
|
||
|
||
```swift
|
||
typealias Fn = (Int) -> Int
|
||
func getFn() -> Fn {
|
||
var num = 0
|
||
func plus(_ i: Int) -> Int {
|
||
num += i
|
||
return num
|
||
}
|
||
return plus
|
||
}
|
||
var fn = getFn()
|
||
print(fn(1)) // 1
|
||
print(fn(2)) // 3
|
||
print(fn(3)) // 6
|
||
```
|
||
|
||
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo2.png" style="zoom:25%">
|
||
|
||
可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的,调用1次 `getFn` 则产生1次堆空间分配,用于保存 num。
|
||
|
||
也就是说如果一个内层函数访问了外层函数的局部变量,也就会发生变量捕获,做法就是延长局部变量的生命周期,在堆空间申请一段内存,用户保存局部变量。
|
||
|
||
对代码进行修改
|
||
|
||
```swift
|
||
typealias Fn = (Int) -> Int
|
||
func getFn() -> Fn {
|
||
var num = 1
|
||
func plus(_ i: Int) -> Int {
|
||
num += i
|
||
return num
|
||
}
|
||
return plus
|
||
}
|
||
var fn1 = getFn()
|
||
var fn2 = getFn()
|
||
var fn3 = getFn()
|
||
print(fn1(1)) // 2
|
||
print(fn2(2)) // 3
|
||
print(fn3(3)) // 4
|
||
```
|
||
|
||
我们在汇编 `swift_allocObject` 下面下个断点
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo3.png" style="zoom:25%">
|
||
|
||
|
||
|
||
第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
|
||
|
||
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo4.png" style="zoom:25%">
|
||
|
||
可以看到内存数据发生了改变。绿色框内有了值1。
|
||
|
||
|
||
|
||
第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
|
||
|
||
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo5.png" style="zoom:25%">
|
||
|
||
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
|
||
|
||
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
|
||
|
||
<img src="./../assets/ClosureCaptureVariableDemo6.png" style="zoom:25%">
|
||
|
||
打印结果也说明了问题,因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存,用于保存捕获的变量。所以调用 fn1 得到 2,调用 fn2 得到 3,调用 fn3 得到 4。
|
||
|
||
BTW,堆空间分配的内存,如果没有 `init` 或者赋特定的值,数据会是随机的。因为有可能是之前别的地方使用过的内存。
|
||
|
||
|
||
|
||
### 闭包内存结构
|
||
|
||
先来个简单的函数,看看指针内存结构
|
||
|
||
```swift
|
||
func sum(_ a: Int, _ b: Int) -> Int { return a + b }
|
||
var fn = sum
|
||
print(fn(1, 2)) // 3
|
||
```
|
||
|
||
在 `var fn = sum` 处下断点,可以看到下面汇编
|
||
|
||
<img src="./../assets/ClouserMemoryLayoutExploreDemo1.png" style="zoom:25%">
|
||
|
||
我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
|
||
|
||
`%rip` 是下一条指令的地址 `0x100003921`,`%rip + 0x10f` 也就是 `0x0000000100003a30`。和猜想一致。
|
||
|
||
第六行汇编的意思就是将 `sum` 函数的地址赋值给 `%rax`。
|
||
|
||
第七行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d8` 处,取8个字节用来保存 `%rax` 的地址。`0x100003928 + 0x88d8 = 0x10000C200`
|
||
|
||
第八行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d85` 处,取8个字节用来保存 `$0x0` 。`0x100003933 + 0x88d5 = 0x10000C208`
|
||
|
||
`0x10000C200` 到 `0x10000C208` 差8位,也是连续的。说明分配了一个函数指针,长度为16位。通过`MemoryLayout.stride(ofValue: sum)` 看到也是16位。符合猜想。
|
||
|
||
|
||
|
||
直奔主题,研究闭包内存
|
||
|
||
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
|
||
|
||
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么,LLDB 输入 `si`
|
||
|
||
<img src="./../assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
|
||
|
||
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算,2个 `ecx` 异或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行
|
||
|
||
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
|
||
|
||
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 `, 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
|
||
|
||
也就是 fn1 前8个字节存放 plus 的函数地址,后8个字节存放0.
|
||
|
||
|
||
|
||
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
|
||
|
||
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
|
||
|
||
基本可以断定:函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
|
||
|
||
汇编第10行,经过在堆上为捕获的变量 alloc 内存后,将内存保存到 `rax` 中,然后赋值给 `rdi`,第11行,将 `rdi` 再赋值给 `rbp - 0x10` 的地址。所以汇编19行的 `rbp - 0x10 ` 保存的也就是堆内存,赋值给 `rdx ` 了。
|
||
|
||
20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
|
||
|
||
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
|
||
|
||
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
|
||
|
||
|
||
|
||
问题变得微妙起来了,`getFn` 方法返回一个地址,占用16个字节,但是前8个字节存储 `plus` 方法地址,后8个字节存储闭包捕获后在堆上申请内存存放的数据地址。那如何根据一个地址的前8位去真正调用方法?
|
||
|
||
|
||
|
||
Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一定不是写死的地址,一定是动态生成的。这种属于间接调用。
|
||
|
||
- call 后面直接加一个函数地址,属于直接调用。类似 `callq 0x100003970`
|
||
- call 后面的函数地址不是固定的,而是动态生成的叫间接调用。在 `at&t` 汇编里面,间接调用前面要加 `*` 。类似 `callq *%rax`,意思是从 `%rax` 里面取出一个地址值出来,当成函数地址,去调用
|
||
|
||
顺着思路,分析下汇编:
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo1.png" style="zoom:25%">
|
||
|
||
我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找,找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来,当作函数地址调用(汇编代码的右边写了,`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
|
||
|
||
断点继续放开,在汇编25行处加断点 `callq *%rax`
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo2.png" style="zoom:25%">
|
||
|
||
可以看到在方法内部,第6行汇编处,直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
|
||
|
||
指令 `jmp`、`call` 的区别在于:
|
||
|
||
- `jmp` 指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。
|
||
- `call` 指令跳转到指定目标地址执行子程序,执行完子程序后,会返回 `call` 指令的下一条指令处执行程序,执行 `call` 指令有堆栈操作过程
|
||
|
||
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo3.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`fn1` 函数调用的时候,参数如何传递?
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo4.png" style="zoom:25%">
|
||
|
||
汇编17行 `movq 0x88d8(%rip), %r13 ; SwiftDemo.fn1 : (Swift.Int) -> Swift.Int + 8`可以看到将返回的 fn1 本身16字节的后8个字节,也就是堆地址空间值,保存到寄存器 `edi` 也就是寄存器 `rdi` 上了。
|
||
|
||
汇编24行 `movl $0x1, %edi` 将参数1传给寄存器 `rdi` 了。
|
||
|
||
然后 LLDB 输入 `si` 去分析 callq 内部
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo5.png" style="zoom:25%">
|
||
|
||
|
||
|
||
可以看到 `movq %r13, %rsi` 将寄存器 r13 的值写入到 `rsi` 了。目前为止:`rdi` 保存参数1,`rsi` 保存堆地址值。
|
||
|
||
继续输入 `si`
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo6.png" style="zoom:25%">
|
||
|
||
可以看到汇编的第5行 `movq %rsi, -0x50(%rbp)` 将 `rsi` 里的1,保存到 `rbp - 0x50` 处。第6行汇编 `movq %rdi, -0x58(%rbp)` 将 `rdi` 堆地址值保存到 `rbp - 0x58` 处。
|
||
|
||
汇编26行 `movq -0x58(%rbp), %rdi` 将 `rbp - 0x58` 的值写入到 `rdi`,也就是堆地址值。第27行 `movq -0x50(%rbp), %rsi` 将 `rbp - 0x50` 的值写入到 `rsi`,也就是参数值1。
|
||
|
||
然后真正做 `plus` 加法运算的就是28行的 `addq 0x10(%rsi), %rdi`, 从 `rsi` 堆地址值的第16个字节的地方取出8个字节的值,也就是捕获的外部变量 `num` 再和参数1相加。
|
||
|
||
<img src="./../assets/ClouseExploreCaptureVariableDemo7.png" style="zoom:25%">
|
||
|
||
可以看到第6行堆地址空间的值写入到 `rbp -0x58` ,第26行又将 `rbp -0x58` 写入到 `rdi`,29行将 `rdi` 的值,写入到 `rbp - 0x48`,34行将 `rbp - 0x48` 写入到 `rcx`,35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
|
||
|
||
|
||
|
||
总结:当 `getFn` 内部没有发生闭包的时候,fn1 的地址就是16 Byte,前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte,前8 Byte 存储间接调用 `plus` 函数的中转函数,后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数:1个是 fn1 函数的参数,1个是堆空间的地址值。
|
||
|
||
```swift
|
||
var fn1 = getFn()
|
||
fn1(1) // 2
|
||
fn1(3) //4
|
||
```
|
||
|
||
因为只调用1次 `getFn` 所以堆内存分配了1个被捕获的变量地址,调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。
|
||
|
||
```swift
|
||
var fn1 = getFn()
|
||
fn1(1) // 2
|
||
fn1(3) //4
|
||
var fn2 = getFn()
|
||
fn2(2) // 3
|
||
fn2(4) // 5
|
||
```
|
||
|
||
因为调用了2次 `getFn` 所以堆内存分配了2个被捕获的变量地址,调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。当调用 fn2 的时候操作的是被捕获的新的一个堆地址空间所指向的变量。
|
||
|
||
且 fn1 所占用的16个字节,fn2 所占用16个字节。2者的前8字节内容相同,都是包装了 `plus` 函数的一个地址。
|
||
|
||
|
||
|
||
### “闭包就是对象”
|
||
|
||
捕获了外部变量的闭包类似于一个类,里面存在存储属性和方法
|
||
|
||
```swift
|
||
class {
|
||
var num: Int
|
||
func fn(_ i: Int) -> Int {
|
||
return i + num
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
## 自动闭包
|
||
|
||
**自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包**。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值
|
||
|
||
这个语法的好处在于通过写普通表达式代替显示闭包,而使你省略包围函数的形式参数的括号.
|
||
|
||
比如系统的断言 `assert`
|
||
|
||
```swift
|
||
public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)
|
||
|
||
```
|
||
|
||
|
||
|
||
```swift
|
||
// 语法糖。自动闭包
|
||
func getPositiveValue(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
|
||
return v1 > 0 ? v1 : v2()
|
||
}
|
||
|
||
//print(getPositiveValue(10, {20}))
|
||
//print(getPositiveValue(-10) {20})
|
||
//print(getPositiveValue(-20) {
|
||
// let a = 10
|
||
// return a + 1
|
||
//}
|
||
//)
|
||
|
||
print(getPositiveValue(-10, 22))
|
||
```
|
||
|
||
`@autoclosure` 会自动将 22 封装成闭包 `{ 22 }`。
|
||
|
||
`@autoclosure` 只支持 `() -> T` 无参数,并且有一个返回值的闭包。
|
||
|
||
`@autoclosure` 并非只支持最后1个参数。
|
||
|
||
`??` 函数的本质就是自动闭包
|
||
|
||
|
||
|
||
自动闭包允许延迟处理,因此任何闭包内部的代码直到你调用的时候才会运行。对于有副作用或者占用资源的代码来说很有用。因为它可以允许你控制代码何时进行求值
|
||
|
||
```swift
|
||
var group = ["zhangsan", "lisi", "wangwu"]
|
||
//print(group.count)
|
||
//let groupRemover = { group.remove(at: 0) }
|
||
//print(group.count)
|
||
//
|
||
////print("execute remove function \(groupRemover())")
|
||
//print(group.count)
|
||
|
||
|
||
func serve(customer customerProvider: @autoclosure () -> String) {
|
||
print("Now serving \(customerProvider())!")
|
||
}
|
||
serve(customer: group.remove(at: 0))
|
||
// Now serving zhangsan!
|
||
```
|
||
|
||
如果一个闭包作为参数,是可以去掉 `{}` 的,参数加了 `@autoclosure` 后,是会自动转换为闭包的。
|
||
|
||
但如果函数参数没有加 `@autoclosure`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
|
||
|
||
<img src="./../assets/SwiftAutoClosureError.png" style="zoom:25%">
|
||
|
||
正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
|
||
|
||
```swift
|
||
// 改法1
|
||
func collectCustomerProviders(_ customerProvider: @escaping () -> String) {
|
||
customerProoviders.append(customerProvider)
|
||
}
|
||
collectCustomerProviders( { group.remove(at: 0) })
|
||
// 改法2
|
||
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
|
||
customerProoviders.append(customerProvider)
|
||
}
|
||
collectCustomerProviders(group.remove(at: 0))
|
||
```
|
||
|
||
如果你的自动闭包允许逃逸,就可以同时使用 `@autoclosure` 和 `@escaping `
|
||
|
||
```swift
|
||
var customerProoviders: [() -> String] = []
|
||
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
|
||
customerProoviders.append(customerProvider)
|
||
}
|
||
collectCustomerProviders(group.remove(at: 0))
|
||
```
|
||
|
||
|
||
|
||
## 闭包和闭包表达式的区别
|
||
|
||
### 闭包
|
||
|
||
定义:闭包是自包含的功能代码块,可以在代码中被传递和使用。它能捕获并存储其所在上下文的常量和变量(即“闭合”环境)
|
||
|
||
种类:
|
||
|
||
- 全局函数(有名称,不捕获任何值)
|
||
- 嵌套函数(有名称,可捕获外曾函数的变量)
|
||
- 闭包表达式(匿名,轻量语法,可以捕获上下文变量)
|
||
|
||
闭包强调的是:捕获上下文的能力。不管是函数还是闭包表达式
|
||
|
||
### 闭包表达式
|
||
|
||
定义:是 Swift 中一种简洁的语法格式,用于编写匿名闭包。是闭包的一种实现方式之一(函数和闭包表达式都可以实现闭包)
|
||
|
||
特点:
|
||
|
||
- 没有函数名
|
||
- 语法清凉,支持参数类型推断、隐式返回、简写参数名(如 $0、$1)等特性
|
||
- 通常用于作为函数的参数传递
|
||
|
||
|
||
|
||
### 总结
|
||
|
||
- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
|
||
- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|