4-7.【协议导向编程】为什么通过协议类型调用方法和通过具体类型调用方法可能行为不同?

3 阅读3分钟

一、先给结论(直接命中要害)

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 决定是否多态

你可以在脑中快速问两句:

  1. 我现在是通过 protocol 看它,还是 concrete type?
  2. 这个方法在 protocol 里吗?

答案一出来,行为就确定了。


八、为什么 Swift 要这样设计?(不是缺陷)

Swift 团队的取舍是:

  • 🔥 性能优先
  • 📦 ABI 稳定
  • 🧠 行为可预测
  • ❌ 避免“隐式 override 的魔法”

如果 extension 方法也动态派发:

  • witness table 会膨胀
  • ABI 复杂
  • 性能不可控

👉 这是一个“工程理性”的设计,不是语言 bug


九、一句话终极总结(记住这句)

通过协议类型调用,你只能得到“协议承诺的行为”,而不是具体类型的全部能力。