避免在 Swift 中使用 `self.`

1,331 阅读3分钟

English Version

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