一、先给结论(直接命中要害)
Swift 中,方法调用行为由“变量的静态类型”决定,而不是运行时的真实类型。
所以:
- 通过具体类型调用 → 编译器知道“你是谁”
- 通过协议类型调用 → 编译器只能看到“你承诺了什么”
这就导致:
👉 同一个对象,在不同“视角”下,调用的可能是不同实现
二、一个最小但致命的例子(必看)
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
👉 编译器知道 p 是 Person
👉 直接调用 Person.greet()
② 通过协议类型调用
let g: Greeter = Person()
g.greet()
❗ 输出:
Hello from protocol extension
🔥 同一个对象,不同行为
为什么?
三、真正原因:Swift 有「两套派发规则」
Swift 不是“统一的多态语言” ,它有三条完全不同的规则:
1️⃣ 具体类型调用 → 静态派发
let p = Person()
- 编译期就知道是
Person - 方法直接绑定
- 不需要任何表
📌 快、确定、可内联
2️⃣ 协议要求方法 → 动态派发(witness table)
protocol Greeter {
func greet()
}
- 方法写在 protocol 里
- 实现可以 override
- 通过协议调用时,查 witness table
📌 这才是“协议多态”
3️⃣ protocol extension 新增方法 → 静态派发(关键点)
extension Greeter {
func greet() { ... }
}
- ❌ 不在 protocol 声明中
- ❌ 不进 witness table
- ❌ 不参与多态
- ✅ 编译期直接绑定
📌 像一个“命名空间里的普通函数”
四、用一句话解释刚才的“怪现象”
let g: Greeter = Person()
g.greet()
编译器在想的是:
“
Greeter协议本身 没有声明greet()
那我就用我在 extension 里看到的那个实现。”
它 根本不知道 Person 里还有一个同名方法。
五、对比:为什么 class 继承不会这样?
class A {
func foo() {
print("A")
}
}
class B: A {
override func foo() {
print("B")
}
}
let x: A = B()
x.foo()
输出:
B
因为:
- class 方法走 vtable
- override 是语言内建概念
- 动态派发是默认行为
👉 协议不是类继承
六、正确的“多态协议”写法(牢记)
✅ 想要行为一致,必须这样写
protocol Greeter {
func greet()
}
extension Greeter {
func greet() {
print("default")
}
}
struct Person: Greeter {
func greet() {
print("person")
}
}
现在:
let g: Greeter = Person()
g.greet()
✅ 输出:
person
因为:
greet()是协议要求- witness table 生效
- 运行时分派
七、工程级判断口诀(非常实用)
变量的“静态类型”决定派发规则
是否写进 protocol 决定是否多态
你可以在脑中快速问两句:
- 我现在是通过 protocol 看它,还是 concrete type?
- 这个方法在 protocol 里吗?
答案一出来,行为就确定了。
八、为什么 Swift 要这样设计?(不是缺陷)
Swift 团队的取舍是:
- 🔥 性能优先
- 📦 ABI 稳定
- 🧠 行为可预测
- ❌ 避免“隐式 override 的魔法”
如果 extension 方法也动态派发:
- witness table 会膨胀
- ABI 复杂
- 性能不可控
👉 这是一个“工程理性”的设计,不是语言 bug
九、一句话终极总结(记住这句)
通过协议类型调用,你只能得到“协议承诺的行为”,而不是具体类型的全部能力。