关于闭包捕获相关的问题在官方文档上并没有写在一起, 系统地整理一下, 复习了一遍.
目录
闭包表达式的基本形式
1
2
3
{ [copyItem = item, anotherItem, unkonwn self] (paraA: Int, paraB: String) -> String in
// some code
}
随便写了一个, 一个完整没有简写的闭包包括捕获列表, 参数类型和返回值, 参数和返回值与普通函数差不多, 就不提了. 捕获列表中可以为被捕获的值起一个别名, 也可以直接直接使用原来的名称(但实际上是闭包外变量的一个拷贝), 为了防止循环引用, 可以在被捕获的引用变量前加weak
, unknown
等修饰.
闭包中默认捕获的是变量的引用
为了下面方便演示, 首先定义下面这个延迟函数:
1
2
3
4
5
6
func delay(_ time: Int = 2, @escaping closure: () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.seconds(time)) {
print("colsure start execute")
colsure()
}
}
这个函数要把传入的闭包延迟两秒执行, 因为在函数执行后才执行闭包, 所以使用@escaping
来修饰闭包. emmmm没搞清 playground 里怎么弄延迟, 所以还是用iOS程序来运行吧.
被捕获的是值类型
1
2
3
4
5
var a = 0
delay {
print("a = \(a)")
}
a = 10
输出为:
1
2
closure start execute
a = 10
可以发现闭包并没有把a
的值复制下来, 而是直接捕获了它的引用, 在闭包执行前修改a
, 执行闭包的时候通过引用找到现在a
的值, 打印10
.
如果使用捕获列表, 那么就会在闭包内对捕获列表中的值进行复制, 闭包内外的变量此时不是同一个变量了.
1
2
3
4
5
var a = 0
delay { [copyA = a] in
print("a = \(copyA)")
}
a = 10
输出为:
1
2
closure start execute
a = 0
等同于下面这么写
1
2
3
4
5
6
var a = 0
var copyA = a // 注意这个变量只可以在闭包内访问
delay {
print("a = \(copyA)")
}
a = 10
被捕获的是引用类型
因为闭包内捕获的是引用, 所以被捕获值本是是引用类型而言, 捕获的并不是引用的值, 而是引用类型本身的一个引用, 有点像C语言中的双重指针.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
var name: String
init(name: String) {
self.name = name
}
deinit {
print("\(self.name) is dead")
}
}
var b = Person(name: "B")
delay {
print("the name of b is \(b.name)")
}
b = Person(name: "C")
输出为:
1
2
3
4
B is dead
closure start execute
the name of b = C
C is dead
和上面值类型的例子差不多, 因为捕获的是引用类型本身的引用, 并没有持有类本身, 所以当给b
赋给其他值的时候, person B
引用计数为0, 进行了析构. 闭包调用的时候, 通过变量b
的引用找到了b
本身, 再跟据b
找到了person C
实例, 输出结果.
如果使用捕获列表, 情况就不一样了, 因为捕获列表是复制一份值到闭包内, 所以闭包内的复制变量会持有类实例的强引用.
1
2
3
4
5
var b = Person(name: "B")
delay { [copyB = b]
print("then name of b is \(b.name")
}
b = Person(name: "C")
输出为:
1
2
3
4
closure start execute
the name of b is B
B is dead
C is dead
因为闭包里的copyB
就是b
的复制, 而不是值引用, 所以同样可以对类实例保留强引用.
引用是可以修改外部变量的
最后, 因为捕获的是引用, 所以如果不是使用捕获列表复制变量的话, 闭包内部是可以修改外部被捕获的变量的
1
2
3
4
5
var a = 0
delay {
a = 10
}
print("a = \(a)")
输出为:
1
2
closure start execute
a = 10
闭包引起的循环引用
闭包本身是引用类型
因为函数, 或者闭包在 swift 中是一等值, 所以闭包是可以以赋值的形式传来传去的, 在传递的过程中, 闭包本身没有进行复制, 传值传的只是闭包自身的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func makeIncrease(_ num: Int) -> () -> Int {
var base = 0
let closure = {
base += num
return base
}
return closure
}
let increase = makeIncrease(10)
increase()
// 10
increase()
// 20
let anotherIncrease = increase
anotherIncrease()
// 30
函数makeIncrease
返回一个闭包, 每执行一个这个闭包, 返回值增加num
. 可以看出当把闭包由increase
传给anotherIncrease
时, 还是同一个闭包, 说明传递的只是闭包的引用.
闭包对引用类型本身的引用是怎么释放的
说起来比较绕口, 还是用上面闭包捕获引用类型的例子来说明, 为了更清晰地看出引用类型什么时候脱离作用域进行析构, 我们把代码放在函数里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
var name: String
init(name: String) {
self.name = name
}
}
func demo() {
var b = Person(name: "B")
delay {
print("the name of b is \(b.name)")
}
b = Person(name: "C")
}
demo()
输出为:
1
2
3
4
B is dead
closure start execute
the name of b is C
C is dead
开始的时候并没有说最下面的一行输出是怎么回事, 当离开demo
的作用域时(此时闭包还没有执行), person C
并没有析构, 因为引用类型变量b
还在被闭包引用着, 而b
保留着对person C
的引用, 所以直到闭包运行结束之后, 变量b
脱离作用域, person C
引用计数为0, 进行析构.
闭包的引用循环
因为闭包本身是引用类型, 可以被类或其他闭包引用; 而闭包本身可以引用类或者其他闭包, 这就可以会构成引用循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
var name: String
lazy var introduce: () -> String = {
let str = "my name is \(self.name)"
return str
}
init(name: String) {
self.name = name
}
}
var a = Person(name: "A")
a.introduce()
// my name is A
a.introduce = {
let str = "I am a person"
}
a.introduce()
// I am a person
introduce
是类Person
的一个变量, 它是一个闭包, 将它声明为懒加载是因为它引用了self
, 而懒加载可以保证只有当self
初始化结束, 并且self
存在的时候才可以调用这个变量.
可以看出, 闭包中引用了self
, 也就是说闭包有对self
的强引用; 而闭包本身又是self
的一个变量, 即self
又强引用了闭包, 这就造成了循环引用, 可以使用unknown
, weak
来修饰这个闭包变量, 或者, 因为我们是在讲闭包, 所以可以在捕获列表中使用unknown
, weak
来避免循环引用:
1
2
3
4
5
6
7
8
9
10
11
class Person {
var name: String
lazy var introduce: () -> String = { [unknown self] in
let str = "my name is \(self.name)"
}
init(name: String) {
self.name = name
}
}
因为类实例析构的时候闭包肯定也不在了, 而使用闭包的时候类一定还在, 所以可以保证在使用期间self
不会为nil
, 在捕获进闭包时使用unknown
修饰self
就可以了.
总结
看这些例子还是有点绕, 其实总结起来注意几点就好:
- 闭包默认捕获的是值的引用
- 如果是引用类型的值, 闭包捕获的是这个引用的引用
- 捕获列表里捕获的是复制进来的值, 而不是引用
- 闭包本身也是引用类型