软件设计

Swift 的 final 关键字为什么应该加在类上(性能与设计总结)

你可能在代码审查中听过“请给这个类加上 final”之类的话。

4 分钟阅读
Swift 的 final 关键字为什么应该加在类上(性能与设计总结) 封面图

你可能在代码审查中听过“请给这个类加上 final”之类的话。

看起来只是习惯,但了解原因后,你看代码的方式会发生变化。

先说结论,final 表示“这个类不打算通过继承进行扩展”。这一行比想象中更有分量,能同时提升性能和设计稳定性。

今天就来总结为什么 Swift 类通常要加上 final,以及背后的真正原因。


final 到底是什么?

final 是用于阻止继承和重写的关键字。

放在类前面时,可以阻止其他类继承该类。

放在方法或属性前面时,也可以只阻止该成员被重写。

final class Logger {
    func log(_ msg: String) { print("[LOG] \(msg)") }
}

let logger = Logger()
logger.log("支付完成")   // 输出: [LOG] 支付完成

如果为了继承 Logger 而写成 class FileLogger: Logger,就会出现编译错误。

也就是把“这个类到此为止”这句话钉死。


加上 final 后为什么会更快?

第一个原因是性能。关键在于分派方式。

只要继承仍然开放,编译器就无法在运行时之前确定实际会调用哪个方法,因此每次都必须查表寻找。

这叫作动态分派。它会先查询每个类的方法表(vtable),再进行调用。

而加上 final 后,情况就不同了。

由于子类无法介入,编译器可以确定“这个调用一定对应这个方法”。

因此无需查表即可直接调用。这就是静态分派。

更进一步,较短的方法甚至可以进行内联,把代码直接展开到调用位置。

一次调用的成本并不高,但如果一个方法在循环中被调用数千次,差异就会累积起来。

一次调用会在这个分岔处分为静态分派或动态分派
一次调用会在这个分岔处分为静态分派或动态分派

真正的原因不是性能,而是“设计”

实际上,我认为这一点比性能更重要。

因为它可以避免脆弱基类问题(fragile base class)。

如果开放继承,任何人都可以自由介入你创建的父类的内部行为。

明明只是无意中修改了父类的一个方法,却可能让所有重写该方法的子类接连崩溃。

继承是最容易破坏封装的关系,因为子类可以看到父类的内部细节。

所以面向对象设计原则也会这样说:

为继承而设计并编写文档;否则就禁止继承。——这是《Effective Java》中的著名建议。

final 就是将这条建议落实到代码中的工具。

编译器会强制执行这样的意图:“既然没有按继承设计,就不要把它开放出去”。

意图明确后,之后阅读代码的同事也会少一些困惑。

不打算开放就使用 final,让编译器守住意图
不打算开放就使用 final,让编译器守住意图

那么是不是所有类都应该加上 final?

这里有一点很重要:在 Swift 中,引用类型不一定非要是类。

如果状态可以按值处理,struct 往往是更好的选择。因为 struct 从一开始就没有继承。

也就是说,在考虑“要不要加 final”之前,应该先问“这真的必须是一个类吗?”

如果仍然需要使用类,就按照下面的标准判断。

情况 判断
没有考虑继承的类 加上 final(默认)
对性能敏感的重复调用代码 使用 final 引导静态分派
设计了明确扩展点的框架 不加 final,保持开放
以继承为前提的 API,例如 UIViewController 不加

总结来说,记住下面几点就很方便。

  • 如果没有继承意图,默认加上 final
  • 只开放你确实想开放的扩展点
  • 如果 struct 就能完成,从一开始就避免使用类

面试时会这样问

Q. 加上 final 关键字有什么好处?

它可以阻止继承和重写,清楚表达设计意图。同时,编译器可以使用静态分派代替动态分派,从而降低调用成本,并支持内联优化。

Q. 那么所有类都应该加上 final 吗?

如果类是为继承而设计并编写文档的,就应该保持开放。不过,没有这种意图的类默认关闭会更安全;如果值语义合适,也应该优先考虑 struct。


final 这一行不只是简单的优化技巧,更是在回答“应该如何使用这个类”。

从今天开始,每次创建新类时都问自己一次:“有什么理由要开放继承吗?”只要养成这个习惯,代码就会稳固得多。

延伸阅读