一、核心原理
协议扩展中的默认实现本质上是静态派发,不是动态多态。
- 静态派发(Static Dispatch) :调用方法时,编译器在编译期就决定调用哪一个实现。
- 动态派发(Dynamic Dispatch / Witness Table) :调用方法时,运行时根据对象实际类型决定调用哪一个实现。
在 protocol extension 中提供默认实现的方法,如果 没有在 protocol 本体中声明:
- 它不会进入协议的 witness table
- 不参与多态
- 调用时只看变量的静态类型
换句话说,默认实现成了“命名空间里的普通函数”,而不是“可被类型覆盖的多态方法”。
二、实例对比(最容易踩坑)
1️⃣ 协议扩展默认实现
protocol Greeter {}
extension Greeter {
func greet() {
print("Hello from protocol extension")
}
}
struct Person: Greeter {
func greet() {
print("Hello from Person")
}
}
调用对比
let p = Person()
p.greet() // 输出: "Hello from Person"
let g: Greeter = Person()
g.greet() // 输出: "Hello from protocol extension"
🔥 同一个对象,行为不一样
原因:
p是具体类型 → 静态派发 → 调用Person.greet()g是协议类型 → 静态派发 → 调用 extension 默认实现- Person.greet() 根本没参与多态
2️⃣ 正确多态写法(必须声明在 protocol 中)
protocol Greeter {
func greet()
}
extension Greeter {
func greet() {
print("Default greet")
}
}
struct Person: Greeter {
func greet() {
print("Person greet")
}
}
let g: Greeter = Person()
g.greet() // 输出: "Person greet"
greet()在协议中声明 → 进入 witness table → 动态派发- 多态行为恢复正常
三、为什么 protocol extension 默认实现破坏多态
-
不在协议中声明
- 编译器只知道 extension 内的实现
- 不创建动态分发表(witness table)
-
静态派发优先于动态派发
- 变量静态类型决定调用路径
- 不看对象实际类型
-
多协议继承或多个 extension
- 同名方法产生“平行实现”,更容易产生不可预测行为
四、工程实践建议
✅ 原则
- 想要多态 → 必须在 protocol 中声明
- protocol extension 只做默认实现 / 工具 / 模板方法
- 多协议继承 → 注意静态派发导致的平行实现
✅ 模板写法(安全)
protocol Renderer {
func draw() // 多态点
}
extension Renderer {
func render() { // 模板方法
setup()
draw() // 多态点
}
func setup() { // 工具方法
print("setup")
}
}
draw()是多态点 → 动态派发render()是模板方法 → 静态派发- 安全且可复用
五、口诀总结
“协议扩展 = 静态工具”,
“协议声明 = 多态契约”,
默认实现不会自动覆盖多态,除非写进协议本体”。