在 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 的强引用。