swift中的闭包是个比较重要的知识点,关于闭包的值捕获理解的总是有点乱,今天有点时间,就把他整理了一下。
1.swift的闭包和OC的Block里值的捕获的区别
先看一段oc代码
1 | NSInteger i = 1; |
当编译器走到第三行,也就是i+=1时候,实际上已经对i进行了拷贝,可以理解成
1 | NSInteger iCopy = i |
所以block里的值和外面的值是不会互相影响的。
如果想要里外一致,则变量需要通过添加 __block关键字修饰,在block里仅仅只是I的指针,拷贝也只是指针的拷贝,这里不详细讨论此关键字以及循环引用相关。
1 | __block NSInteger i = 1; |
再看一断swift代码
1 | var i = 1 |
会发现打印的都是一样的。
再看一段swift代码:
1 | var i = 1 |
结论1:swift值的捕获是在执行的时候再捕获,当代码执行到closure(),对值进行捕获,i此时的值为几,所以捕获到的i就是几。
如果swift里要实现和oc一样的默认的值捕获呢,在这里swift引入了捕获列表的概念,如下代码:
1 | var i = 1 |
闭包在定义时⽴即捕获列表中所有变量,并将捕获的变量⼀律改为常量,供⾃己使用。
2.闭包值捕获的几种类型
说明:闭包捕获等同于 copy ,闭包捕获某个变量就意味着 copy 一份这个变量,值类型的变量直接复制值,引⽤用类型的变量复制引用。复制后的变量名同被捕获的变量,复制后的变量仍为变量,常量仍为常量。
值类型捕获
1
2
3
4
5
6
7
8
9
10
11
12
13struct Demo {
var name: String
func printName() -> () -> () {
return {
print(self.name)
}
}
}
var demo = Demo(name: "a")
let closure = demo.printName() // 1
demo.name = "b"
closure()结构体 Demo 的实例⽅法 printName() 返回⼀个捕获了 self 实例本身的闭包, Demo 为值类型,因此 // 1 ⾏代码执⾏完成后,闭包 closure 复制了⼀份存储在变量 demo 中 名为 a 的 Demo 实例,那么当存储在变量 demo 的 Demo 实例改名为 b 时,闭包 closure 所捕获的 Demo 实例不变,名字仍为 a ,因此输出结果为: a 。
引用类型捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Demo {
var name: String
init(name: String) {
self.name = name
}
func printName() -> () -> () {
return {
print(self.name)
}
}
}
var demo = Demo(name: "a")
let closure = demo.printName() // 1
demo.name = "b"
closure()
这次 Demo 类型为类,是引⽤用类型,因此 // 1 ⾏代码执行完成后,闭包 closure 复制了一份变量 demo 所指向的名为 a 的 Demo 实例引用,此时变量 demo 与闭包 closure捕获的 demo 指向同⼀ Demo 实例,那么当变量demo 所指向的 Demo 实例改名为 b时,闭包 closure 所捕获的 Demo 实例名字也改为 b ,因此输出结果为: b 。
- 引用类型变量被捕获后的特性
说明:引⽤类型变量被捕获意味着变量所指向的类的引⽤被复制,也即引用计数会加一,因此为强持有
1 | class Demo { |
闭包 closure 捕获了变量 demo 所指向的 Demo 实例,⽽引⽤类型闭包捕获为强持有,因此变量 demo 所指向的 Demo 实例的引⽤计数为 2,那么当在 // 1 行设置变量 demo 为 nil 时, demo 所指向的 Demo 实例的引用计数减为 1,并不销毁,因此输出结果为: a
闭包捕获发生的时机
当闭包所使用外部变量的作⽤域未结束时,闭包只是简单使用外部变量,并不捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class Demo {
var name: String
init(name: String) {
self.name = name
}
func printName() -> () -> () {
return {
print(self.name)
}
}
}
func doSomething() {
// 局部上下文1
var demo: Demo? = Demo(name: "a")
func printName() -> () -> () {
// 局部上下文2
return {
print(demo?.name)
}
}
let closure = printName() // 1
closure() // 2
demo?.name = "b"
closure() //3
demo = nil // 4
closure() // 5
}
doSomething()函数 doSomething() 的内部函数 printName() 返回⼀个闭包,被返回的闭包定义在局部上下文 2 中,并使用了局部上下文 1中的变量 demo 。虽然 // 1行变量 closure 存储了了内部函数 printName() 返回的闭包,但这个闭包从初始化到销毁整个⽣命周期中,并未脱离其使用的外部变量 demo 的作用域即局部上下文 1,那么闭包 closure 并不不捕获外部变量 demo 。因此当 // 4行设置 demo 为 nil 时,变量 demo 所指向的 Demo 实例变量被销毁,最终的输出结果为:
1
2
3Optional("a")
Optional("b")
nil
如果闭包所使用的外部变量的作用域结束,⽽闭包或因被返回,或作为参数传递给其他函数⽽仍然存在时,闭包⾃动捕获其使⽤的外部变量。
将上面doSomething方法改为如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16func doSomething() {
var demo: Demo? = Demo(name: "a")
func printName() -> () -> () {
// 局部上下⽂文2
let demo1 = demo
return {
print(demo1?.name)
}
}
let closure = printName() // 1
closure() // 2
demo?.name = "b"
closure() //3
demo = nil // 4
closure() // 5
}当这个闭包被返回时,其使用的外部变量 demo1 的作⽤域即局部上下文2也同时结束,因此变量 demo1 被捕获。那么 // 1 ⾏执行结束后,闭包 closure 捕获了变量 demo1 ,则变量 demo 所指向的名为 a 的 Demo 实例的引用计数为 2,当在 // 4 行 设置 demo 为 nil 时,其指向的名为 a 的 Demo 实例的引用计数只是降为 1 ⽽而已,并不销毁,因此最后的输出结果为:
1
2
3Optional("a")
Optional("b")
Optional("b")
结论2:
- 闭包捕获某个变量等于 copy ⼀份这个变量,值类型的变量直接复制值,引用类型的变量直接复
制引用值,与函数中参数传递类似,复制后的变量名同被捕获的变量。 - 如果闭包所使用的外部变量的作用域未结束,闭包只是简单使用这些外部变量,并不捕获。
- 闭包捕获发生在闭包所使用的外部变量的作用域结束,而闭包或因被返回,或作为参数传递给其他函数而仍然存在时。