在 Swift 中,开发者通常习惯在类实例中使用 self. 前缀来调用方法和访问属性。虽然这种明确性对代码理解可能有所帮助,但它也可能无意中导致循环引用(内存泄漏的常见原因)。本文探讨了为什么当 Swift 编译器不需要时,通常最好避免使用 self.(尤其是在闭包中)。
这不仅仅是个人偏好的问题,它也符合 Swift 社区的发展方向,正如最近的语言更新所示:
- SE-0269 (Swift 5.3): 此提案允许在显式捕获
self(例如[self])或self是值类型的闭包中省略self.。它减少了样板代码,并提高了可读性。 - SE-0365 (Swift 5.8): 此提案允许在弱引用捕获
self(例如[weak self])的闭包中,在self被安全解包后(例如,使用guard let self else { return }),省略self.。它进一步简化了涉及弱引用的代码。
得益于这些 Swift 的演进,我们现在可以在许多情况下避免显式使用 self.。这使得代码更简洁,更重要的是,通过使捕获语义更清晰,有助于防止诸如循环引用之类的错误。
例如:
class MyClass {
func func1() {
let closure1 = { [weak self] in
guard let self else { return }
/* Do something else */
let closure2 = {
self.func2()
}
}
}
func func2() {}
}
你能发现潜在的问题吗?你可能会注意到 closure2 没有弱引用捕获 self。但在更复杂代码的实际场景中,这种疏忽很容易被忽视并导致循环引用。
幸运的是,通过删除显式的 self. 并依赖编译器,我们可以捕获此问题:
class MyClass {
func func1() {
let closure1 = { [weak self] in
guard let self else { return }
/* Do something else */
let closure2 = {
func2() // Error: Call to method 'func2' in closure requires explicit use of 'self' to make capture semantics explicit
}
}
}
func func2() {}
}
编译器立即标记一个错误,指示在 closure2 内部调用 func2 需要显式捕获 self。这是因为,如果没有为 closure2 提供显式捕获列表,使用 self.func2() 则会在 closure2 内部隐式创建对 self 的强引用,如果 closure1 也被强持有,则可能导致循环引用。
要解决此问题,我们将 [weak self] 添加到 closure2 的捕获列表中。如果 closure1 中没有对 self 的强引用,我们也可以从 closure1 中删除 guard let self。
class MyClass {
func func1() {
let closure1 = { [weak self] in
/* Do something else */
let closure2 = { [weak self] in
guard let self else { return }
func2()
}
}
}
func func2() {}
}
这些调整,解决了编译器错误,也消除了潜在的循环引用。这种方法有效地利用了编译器的诊断功能,减轻了开发者手动跟踪这些引用语义的认知负担。
总结一下,关键要点/指导原则:
- 当编译器不需要时,优先省略
self.: 这在类内部的闭包中尤其重要。尽可能让编译器推断self。 - 明确定义捕获语义: 当闭包需要访问
self时,始终使用显式捕获列表(例如[self]或[weak self])。这使关于self生命周期和所有权的意图清晰明了。 - 在解包弱引用后拥抱隐式
self(SE-0365): 一旦安全地解包了weak self(例如,通过guard let self else { return }),就可以并且应该在该作用域内后续成员访问时省略self.。显式编写self.可能会意外地在闭包中创建对 self 的强引用。