每天了解一点不一样的 Swift 小知识
代码截图
小笔记
这段代码在说什么
在处理逃逸闭包内部的逻辑时,我们通常会使用 weak self
的方式来避免循环引用。为了在闭包里面正确的使用 self
变量,我们需要通过可选绑定的方式将原先的 self
重新命名并使用。
在 Swift 4.2 之前,self
是全局保留关键字,所以如果在逃逸闭包中把 self
标记为 weak
后,还想继续使用 self
就需要使用 将
self` 包起来:
guard let `self` = self else { return }
而在 Swift 4.2 之后,基于 Allow using optional binding to upgrade self from a weak to strong reference 提案,可选绑定中的 self
不再作为保留关键字。我们完全可以光明正大的这么写了:
guard let self = self else { return }
内存泄漏与闭包里的 self
在开始下面的内容前,我们先回顾一下内存泄漏和闭包里的 sefl 这个话题。
假设我们有这样一段代码:
doSomething(then: {
// do something else
})
由于 Swift 的内存管理机制,如果我们在闭包里面使用一些局部变量的话,闭包就会捕获这个变量,就像下面的代码一样:
var dog = Dog()
doSomething(then: {
dog.bark() // dog must be captured so it will live long enough
})
Swift 编译器会自动管理 dog 的引用计数,这个 dog 会被 doSomething
的闭包 捕获,从而增加引用计数,即使跳出这段代码的作用域,dog 实例仍然会被 doSomething
的闭包所持有。
这种形式在上面的例子中似乎并无大碍,但有时候却会带来一些问题。最常见的一种情况就是循环引用。
试想一下:某个实例拥有一个闭包属性,而这个属性又会调用自身的一些实例方法或者属性时,它们就会产出如下图所示的引用关系。
根据前面的例子,我们知道只要闭包不被销毁,它就会一直持有 self,而另一边,闭包作为实例的一个属性,它们是同生死,共存亡!
你瞧,它们谁也无法释放对方……然后,内存泄漏了!
避免循环引用
为了避免循环引用,Swift 提供了捕获列表来让开发者决定如何持有相关变量。拿之前的 dog 代码举例说明,它就变成了如下的样式:
let dog = Dog()
doSomething(then: { [weak dog] in
// dog is now Optional<Dog>
dog?.bark()
)
此时,当我们再次跳出这段代码的作用域后,闭包里 dog 将变为 nil。除非我们还在其他地方,通过一些途径强持有了 dog 实例。
同理,捕获列表也支持 self:
doSomething(then: { [weak self] in
self?.doSomethingElse()
)
不过由于此时的 self 变成了一个可选类型,所以如果我们要区分不同状态下的代码行为,我们通常还会在闭包内部创建一个对 weak self
的强引用。
doSomething(then: { [weak self] in
guard let strongSelf = self { else return }
strongSelf.doSomethingElse()
)
在上面的这个例子中,我们确保了 strongSelf
是一定有值的,而且当 self
为 nil 的时候,不会执行剩下的代码。
可选绑定里的 self
虽然在许多 Swift 项目中能够看到 strongSelf
的写法,但开发者还是觉得这种方式有点不那么时髦,总想着要弄点什么新鲜玩意儿来!
果不其然,Swift 社区里的人发现了在闭包里还可以进行如下的操作:
guard let `self` = self else { return }
这种 ‘trick’ 的方式能够让我们统一 self 的写法,也让代码看起来变得更顺眼了一些。
但有意思的是,我们的 Chris,也就是 Swift 之父,亲口承认了这个所谓的“新特性”,其实是一个 Swift 编译器的 bug 。
但社区里的开发者并没有打算放弃努力!
终于,在 Swift 4.2 的时候,Swift 团队接受了开发者提出的相关 proposal 并提供了在可选绑定中不再将 self 作为保留关键字的特性。所以我们现在能够看到如下的写法:
guard let self = self else { return }
当然取消了这个限制后也意味着 self 可能不一定是 self 了:
var number: Int? = nil
if let self = number {
print(self) // 这里的 self 是 number:Int
}
所以,即使如此,还是希望大家在正确的地方将 self 作为可选绑定的变量名,免得造成其他的困扰。