本文基于 Sagar Unagar 的《Static vs Dynamic Dispatch in Swift - How Method Dispatch Really Works》(2026-02-21)学习整理,原文链接见文末。
方法分派(Method Dispatch)是 Swift 里一个偏底层、却默默影响着性能、架构与 API 设计的概念。大多数 App 即使从不去想"分派"也能跑得好好的,但理解 Swift 究竟如何决定"调用哪一个方法实现",能帮你写出更可预测、更高效、更易维护的代码------尤其是在构建框架、可复用组件,或是性能敏感的功能时。
本文会讲清楚:
- 分派在 Swift 中到底指什么
- 静态分派 与动态分派的区别
- 结构体、类、协议、泛型分别是如何影响分派的
- Swift 依据什么规则选择分派方式
- 面向分派机制设计 API 的实用准则
目标不是让你过早优化,而是建立起"Swift 究竟如何执行你的代码"的正确心智模型。
一、什么是 Swift 中的"分派"
在 Swift 里,分派(dispatch)指的是:当调用一个方法时,编译器与运行时如何确定到底该执行哪一个函数实现。
看这个例子:
swift
// 定义一个 Greeter 协议,要求实现 greet() 方法
protocol Greeter {
func greet() -> String
}
// 英文问候者,遵循 Greeter
struct EnglishGreeter: Greeter {
func greet() -> String {
"Hello"
}
}
// 用协议类型来接收具体实例(存在类型,类型信息被擦除)
let greeter: Greeter = EnglishGreeter()
print(greeter.greet()) // 输出:Hello
在调用点 greeter.greet() 处,Swift 必须决定运行哪一个具体的 greet() 实现。这个决定有两种产生方式:
- 编译期就定好 → 静态分派(Static dispatch)
- 运行期再决定 → 动态分派(Dynamic dispatch)
理解这两种时机分别什么时候发生,是理解 Swift 性能模型与抽象模型的关键。
二、静态分派(Static Dispatch)
静态分派 意味着:编译器在编译期就已经确切知道要调用的方法实现,并直接生成对该函数的调用指令,运行期没有任何查找过程。
1. 值类型上的静态分派
swift
// 结构体是值类型,无法被继承
struct Counter {
// 方法没有 override 的可能,编译器确切知道调用目标
func increment(_ value: Int) -> Int {
value + 1
}
}
let counter = Counter()
print(counter.increment(10)) // 输出:11
因为 Counter 是值类型且不可被继承,Swift 确切知道该调用哪个 increment(_:)。编译器可以激进地 内联(inline) 并优化这次调用。这也是 Swift 推荐用 struct / enum 来建模数据与行为的重要原因之一。
2. final 类上的静态分派
swift
// final 类:明确禁止被继承
final class Logger {
func log(_ message: String) {
print(message)
}
}
let logger = Logger()
logger.log("Hello") // 输出:Hello
将类标记为 final,等于告诉编译器这个方法不可能被重写。即便它是类方法,Swift 也能因此改用静态分派。
3. 静态分派的好处
静态分派让编译器能够:
- 内联(inline) 方法调用,消除函数调用开销
- 去除抽象层开销(如协议/泛型的间接层)
- 执行跨函数优化(过程间优化)
- 减小二进制体积
- 在热点路径(hot path)上提升性能
在很多情况下,经过优化后这次调用甚至会完全消失(被直接替换为常量或内联代码)。
三、动态分派(Dynamic Dispatch)
动态分派 意味着:方法实现要等到运行期 ,根据实例的真实类型才能确定。当 Swift 在编译期无法知道具体实现时,就必须用到它。动态分派带来了多态能力,但也引入了运行期开销。
1. 类继承中的动态分派
swift
class Animal {
func speak() -> String {
"..." // 默认实现
}
}
class Dog: Animal {
override func speak() -> String {
"Woof"
}
}
// 静态类型是 Animal,但运行期实际类型是 Dog
let animal: Animal = Dog()
print(animal.speak()) // 输出:Woof
这里:
- 静态类型(编译期类型) 是
Animal - 运行期类型(动态类型) 是
Dog
Swift 必须在运行期判断出正确的方法实现。它底层使用的是 Swift 的类分派机制(虚函数表 / v-table)。
2. 协议存在类型(Protocol Existentials)上的动态分派
swift
protocol Storage {
func save()
}
struct DiskStorage: Storage {
func save() {
print("Saved to disk")
}
}
// 协议作为类型使用,具体类型信息被"擦除"
let storage: Storage = DiskStorage()
storage.save() // 输出:Saved to disk
当协议被当作类型使用时,Swift 会擦除(erase)具体的类型信息,方法调用要在运行期通过一次查找机制来解析。这种灵活性让你能写出面向抽象的代par码,但会付出一点运行期代价。
四、协议、泛型与分派
协议在 Swift 里既可能走静态分派,也可能走动态分派------取决于你怎么用它。
1. 协议作为泛型约束 → 静态分派
swift
protocol Greeter {
func greet() -> String
}
struct EnglishGreeter: Greeter {
func greet() -> String { "Hello" }
}
// 泛型函数:G 受 Greeter 约束,但具体类型在编译期已知
func greet<G: Greeter>(_ greeter: G) {
print(greeter.greet())
}
greet(EnglishGreeter()) // 输出:Hello
因为编译器会为每一个具体类型特化(specialize)这个函数,方法调用是静态分派的。这让编译器可以激进内联与优化。
2. 协议作为类型(any) → 动态分派
swift
protocol Greeter {
func greet() -> String
}
struct EnglishGreeter: Greeter {
func greet() -> String { "Hello" }
}
// 使用 any Greeter:存在类型,类型信息被擦除
func greet(_ greeter: any Greeter) {
print(greeter.greet())
}
greet(EnglishGreeter()) // 输出:Hello
这里具体类型被擦除了,Swift 必须做运行期查找才能确定调用哪个实现。
💡 一句话记忆:泛型约束走静态,存在类型(any P)走动态。
五、@objc 与 dynamic 如何改变分派
当你给方法加上 @objc 或 dynamic,Swift 会切换到 Objective-C 运行时分派:
swift
import Foundation
class Player: NSObject {
// @objc dynamic:走 Obj-C message send 机制
@objc dynamic func play() {
print("Playing")
}
}
let player = Player()
player.play() // 输出:Playing
它使用的是:
- Objective-C 消息发送(msgSend)
- 运行期方法查找
- 兼容 KVO(键值观察)
这种分派方式比 Swift 的 v-table 分派更慢,应当只在确实需要以下场景时使用:
- KVO 键值观察
- 方法交换(method swizzling)
- Objective-C 互操作
六、分派行为汇总表
| 模式 | 分派类型 |
|---|---|
泛型约束(<T: P>) |
静态分派 |
协议作为类型(存在类型 P) |
动态分派 |
| 结构体 / 枚举方法 | 静态分派 |
final 类方法 |
静态分派 |
| 可重写的类方法 | 动态分派 |
@objc dynamic 方法 |
Obj-C 消息发送(动态,最慢) |
七、Swift 如何选择分派方式
Swift 遵循几条通用规则:
- 如果方法不可能被重写,Swift 优先用静态分派
- 如果方法可能被重写,Swift 用动态分派
- 如果类型信息被擦除(协议存在类型),Swift 用动态分派
- 如果编译器能特化调用(泛型),Swift 优先用静态分派
Swift 的设计哲学是:只要安全,就尽量用静态分派。 静态分派是默认偏好,动态分派是"被迫"才启用。
八、实践设计指南
适合优先用静态分派的场景
- 写性能敏感的代码
- 实现算法与数据转换
- 设计库与可复用组件
- 在紧密循环(tight loop) 内部
优先选择:
- 结构体与枚举
- 泛型
- 不打算被继承时的
final类
适合用动态分派的场景
- 建模多态的领域行为
- 设计插件式系统
- 在多个运行期实现之间做抽象
- 构建依赖注入边界
动态分派能提升灵活性与架构清晰度。
九、常见误区
误区 1:「Swift 永远用静态分派」 Swift 只是偏好静态分派;当运行期多态确实需要时,它会回退到动态分派。
误区 2:「动态分派很慢,应当避免」 动态分派确实比静态分派稍慢一点,但在真实应用里这点成本通常可以忽略不计。架构清晰度往往比微观优化更重要。
误区 3:「协议很慢」 协议只有在被当作类型(存在类型)使用时 才走动态分派。配合泛型使用时,协议是静态分派且高度优化的。
十、总结
分派是 Swift 性能模型与抽象模型的基石概念。理解 Swift 在何时、以何种方式选择静态 vs 动态分派,能帮你:
- 设计更好的 API
- 避免意外的性能成本
- 构建更可预测的架构
- 理性地思考优化行为
你不需要优化每一个调用点------但知道分派是怎么工作的,能让你做出有意识的设计选择,而不是被编译器"随机"安排。
我的见解与扩展场景
1. 这不是"二选一",而是一种"默认偏好"
Swift 的分派选择本质上是一个保守优化:编译器默认尝试静态分派,只有当存在"无法在编译期确定实现"的证据(可重写、类型擦除)时才退化为动态。理解这一点,你就不会再纠结"我到底该用 struct 还是 class"------答案取决于你是否需要多态,而不是性能恐慌。
2. 协议为何"既快又慢"
这是初学者最容易困惑的点。同一个 Greeter 协议:
- 用在泛型
<T: Greeter>上 → 编译期单态化(monomorphization),零开销,和直接调EnglishGreeter.greet()一样快; - 用在
any Greeter上 → 装箱成存在容器(existential container),多一次 v-table 查找。
所以"协议慢"是个伪命题,是用法决定了快慢 。在性能热点里,把 any Protocol 改成泛型约束往往能直接消除动态分派开销,而不需要改动业务逻辑。
3. 与 Swift 6 严格并发的隐性关联
静态分派在并发安全上也更友好:值类型 + 静态分派天然避免了许多跨 actor 共享可变状态的隐患。当你纠结某段代码为什么过不了 Sendable 检查时,回过头看是不是因为用了类继承 + 动态分派把可变状态"藏"进了抽象层------很多时候换成值类型 + 泛型,问题会自然消失。
4. 扩展场景:写框架时的"分派预算"
在写 SDK / 框架时,我习惯做一张"分派预算表":对外暴露的公开 API 大多需要动态分派(为了可扩展、可重写),但框架内部的算法与数据转换路径尽量保持 struct + final + 泛型,把动态分派限制在少数几个"扩展点"上。这样既给了用户灵活性,又守住了性能底线。
5. 一个可动手验证的小实验
你可以用 Swift 的 -O 优化 + 反汇编(或 Compiler Explorer)观察:把 final 去掉、或把泛型函数改成 any 参数,调用点从 call 直接调用变成了 vtable 间接跳转。亲眼看到这层差异,比读十遍文章都管用。
swift
// 实验:对比以下两种写法在 -O 下的生成的调用指令
func direct(_ c: Counter) { c.increment(1) } // 静态:可能直接内联
func erased(_ c: any Incrementable) { c.increment(1) } // 动态:存在容器 + vtable 查找
参考资料
- 原文:Static vs Dynamic Dispatch in Swift - How Method Dispatch Really Works --- Sagar Unagar(2026-02-21)
- Swift 官方文档: The Swift Programming Language - Methods
- Swift 官方文档: The Swift Programming Language - Protocols
- 关于存在类型与泛型的官方演化提案:SE-0352: Implicitly Opened Existentials