4-12.【协议导向编程】为什么 protocol extension 中提供默认实现可能破坏多态行为?

3 阅读2分钟

一、核心原理

协议扩展中的默认实现本质上是静态派发,不是动态多态。

  • 静态派发(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 默认实现破坏多态

  1. 不在协议中声明

    • 编译器只知道 extension 内的实现
    • 不创建动态分发表(witness table)
  2. 静态派发优先于动态派发

    • 变量静态类型决定调用路径
    • 不看对象实际类型
  3. 多协议继承或多个 extension

    • 同名方法产生“平行实现”,更容易产生不可预测行为

四、工程实践建议

✅ 原则

  1. 想要多态 → 必须在 protocol 中声明
  2. protocol extension 只做默认实现 / 工具 / 模板方法
  3. 多协议继承 → 注意静态派发导致的平行实现

✅ 模板写法(安全)

protocol Renderer {
    func draw()  // 多态点
}

extension Renderer {
    func render() {  // 模板方法
        setup()
        draw()       // 多态点
    }

    func setup() {  // 工具方法
        print("setup")
    }
}
  • draw() 是多态点 → 动态派发
  • render() 是模板方法 → 静态派发
  • 安全且可复用

五、口诀总结

“协议扩展 = 静态工具”,
“协议声明 = 多态契约”,
默认实现不会自动覆盖多态,除非写进协议本体”。