1️⃣ 静态派发(Static Dispatch)
特点
- 编译期确定调用目标函数
- 零开销(没有额外指针查找)
- LLVM 可以进一步 内联、向量化、优化 CoW
一定是静态派发的情况
| 场景 | 示例 | 说明 |
|---|---|---|
| 普通函数/全局函数 | func foo() { ... } | 编译器知道函数地址 |
| 结构体/枚举的普通方法 | struct S { func bar() {} } | 值类型方法默认静态派发 |
| 泛型函数特化 | func add<T: Numeric>(_ a: T, _ b: T) -> T { a + b },T 已知 | 泛型特化生成的具体函数,静态调用 |
| final class 方法 | final class C { func baz() {} } | final 修饰 → 不可被子类重写 → 编译器可静态调用 |
| 内联函数/编译期已知类型 | @inline(__always) func f() {} | LLVM 可直接内联展开 |
总结:
值类型方法、非
dynamic的函数、final类方法、特化泛型 → 静态派发
2️⃣ 动态派发(Dynamic Dispatch)
特点
- 调用目标在运行期确定
- 通过 vtable / witness table / objc_msgSend 间接调用
- 有额外开销(CPU 多一次指针跳转)
一定是动态派发的情况
| 场景 | 示例 | 说明 |
|---|---|---|
| 非 final class 的普通方法 | class C { func foo() {} } | 子类可重写 → 通过 vtable 调用 |
| @objc 方法 | @objc func bar() {} | Objective-C runtime 动态派发 → objc_msgSend |
| 协议存在类型方法(existential) | protocol P { func p() } let x: P = ...; x.p() | 通过 witness table 动态调用 |
| 动态属性 | @objc dynamic var name: String | 每次访问都是动态派发 |
| 泛型函数在调用方类型未知 | func f<T>(_ x: T) { ... },T 未具体化 | 无法特化,只能使用通用泛型代码 → 有动态 type metadata 调用 |
总结:
类方法可被子类重写、@objc/dynamic 方法、协议存在类型方法、未特化泛型 → 动态派发
3️⃣ 派发方式对比表
| 特性 | 静态派发 | 动态派发 |
|---|---|---|
| 调用时机 | 编译期确定 | 运行期确定 |
| 性能 | 高,直接调用 | 低,间接调用(vtable / witness table / objc_msgSend) |
| 可优化性 | 可内联、向量化、消除 CoW | 内联受限,LLVM 难以优化 |
| 典型场景 | 值类型方法、final class、泛型特化 | 非 final class、协议存在类型、@objc/dynamic |
| 可预测性 | 完全可预测 | 受运行时类型影响 |
4️⃣ 核心原则
- 值类型方法 → 静态派发
- final 修饰的 class 方法 → 静态派发
- 普通 class 方法 → 动态派发
- 协议存在类型(existential) → 动态派发
- 泛型函数特化 → 静态派发;未特化 → 通用泛型动态派发
- @objc / dynamic → 动态派发
总结一句话:可在编译期确定调用目标 → 静态派发;必须运行期判断类型 → 动态派发