1️⃣ 现象示例
protocol P {
func foo()
}
extension P {
func foo() {
print("protocol extension foo")
}
}
struct S: P {}
let s: S = S()
s.foo() // 输出: protocol extension foo
let p: P = S()
p.foo() // 输出: protocol extension foo
为什么看似“多态”却不是?
- 协议扩展默认实现是在 静态派发(Static Dispatch)
- 即便
p类型是协议类型,也不会调用结构体 S 自己的方法(除非 S 显式实现foo)
2️⃣ 原理分析
(1)协议扩展方法是静态分发
- 扩展方法在编译期解析调用目标
- 调用点类型决定调用的实现
let s: S = S()
s.foo() // S 类型已知 → 静态调用 P extension 的实现
- 不是通过 witness table → 不是动态派发
(2)协议存在类型调用规则
-
协议类型变量(existential)调用协议要求的方法:
- 只有 协议本身声明的方法 并且被类型覆盖,才动态派发
- 如果只是协议扩展提供的默认实现 → 静态派发,不走动态派发
struct S2: P {
func foo() { print("S2 foo") }
}
let p2: P = S2()
p2.foo() // 输出: S2 foo(动态派发,因为 S2 覆盖了协议要求方法)
-
结论:
- 默认实现不会被协议存在类型的动态派发捕获
- 只有显式在类型上实现的方法才参与动态派发
(3)多态 vs 静态分发对比
| 情况 | 调用类型 | 是否动态派发 | 输出 |
|---|---|---|---|
| S 没实现 foo | s: S | 静态派发 | protocol extension foo |
| S 没实现 foo | p: P | 静态派发 | protocol extension foo |
| S 显式实现 foo | p: P | 动态派发 | S 的实现 |
核心差异:协议扩展的默认实现只参与静态派发,不参与多态动态派发
3️⃣ 典型问题场景
- 多态调用期望失效
func callFoo(_ p: P) {
p.foo() // 期望多态,但如果 p 的类型没有覆盖协议方法 → 调用扩展方法(静态派发)
}
- 泛型函数与协议约束结合时
func genericCall<T: P>(_ t: T) {
t.foo() // 泛型 T 已知 → 静态派发扩展方法
}
- 即使不同类型调用,仍可能调用同一静态实现 → “非多态”
4️⃣ 如何解决 / 避免
- 在类型上显式实现协议方法
struct S: P {
func foo() { print("S foo") }
}
- 这样协议存在类型调用会动态派发 → 多态正确
- 只用协议扩展方法做默认实现,不依赖多态
protocol P {
func foo()
}
extension P {
func foo() { print("default") }
}
// 泛型 T: P 调用 foo → 编译期静态调用
- 用来提供 默认行为,不是多态行为
- 协议继承 + 覆盖设计
- 如果需要真正多态,可以使用类 + 协议结合,或者显式在 struct/enum 上实现方法
5️⃣ 总结
协议扩展默认实现的“看似多态,实际非多态”问题本质是静态派发:
- 默认实现属于 静态分发,调用点类型决定实现
- 只有显式在类型上实现协议要求方法,才会走 动态派发(witness table)
- 泛型函数、协议存在类型调用和默认实现结合时,很容易产生“非多态行为”