4-10.【协议导向编程】在多协议继承时,protocol extension 的静态派发可能带来哪些坑?如何解决?

2 阅读3分钟

多协议继承 + protocol extension + 静态派发,如果没想清楚,非常容易写出“看起来合理、运行却诡异”的代码

我分四步来讲,保证你听完能 一眼识别坑、知道怎么拆

  1. 坑从哪来(本质原因)
  2. 典型翻车场景(真实会遇到)
  3. 为什么会这样(编译器视角)
  4. 可落地的解决方案(工程级)

一、坑的根源一句话版(先记住)

多协议继承时,protocol extension 中“同名方法”不会形成 override 关系,
而是多个彼此无关的“静态实现”。

👉 谁的实现被调用,取决于“静态类型视角”,不是对象真实能力。


二、典型坑 1:多个协议 extension 定义了同名方法

❌ 翻车示例(非常常见)

protocol A {}
protocol B {}

extension A {
    func foo() {
        print("foo from A")
    }
}

extension B {
    func foo() {
        print("foo from B")
    }
}

struct X: A, B {}

你可能以为:

X 同时 conform A 和 B,
foo() 会“自动选一个更具体的”?

现实是:根本不自动


调用行为(重点)

let x = X()
x.foo() // ❌ 编译错误:Ambiguous use of 'foo'

🔥 因为编译器看到两个完全独立的 foo()


换个“看似合理”的写法(更隐蔽)

let a: A = X()
a.foo()   // foo from A

let b: B = X()
b.foo()   // foo from B

同一个对象,行为不一致


💥 坑点总结

  • extension 方法是静态派发
  • 不存在“override 层级”
  • 多协议 = 多套平行实现
  • 行为取决于你“站在哪个协议视角”

三、典型坑 2:子协议“以为”能 override 父协议 extension

❌ 错误直觉

protocol Base {}

extension Base {
    func foo() {
        print("Base foo")
    }
}

protocol Child: Base {}

extension Child {
    func foo() {
        print("Child foo")
    }
}

struct S: Child {}

你可能以为:

Child 的 foo() 会覆盖 Base 的 foo()


实际行为(非常反直觉)

let s = S()
s.foo()

❗ 输出:

Base foo

😱 Child 的实现完全没生效


为什么?

  • foo() 不在 protocol 声明中
  • Base.foo / Child.foo 是两个平行静态函数
  • 编译器优先选择“最早可见的实现”
  • 不存在 override 链

四、典型坑 3:多协议 + 默认实现 + existential

protocol Logger {}
protocol FileLogger: Logger {}

extension Logger {
    func log() {
        print("log from Logger")
    }
}

extension FileLogger {
    func log() {
        print("log from FileLogger")
    }
}

struct MyLogger: FileLogger {}

调用结果对比

let l1: Logger = MyLogger()
l1.log() // Logger

let l2: FileLogger = MyLogger()
l2.log() // FileLogger

🔥 同一个类型,行为完全不同


五、为什么这些坑一定会出现?(本质)

Swift 在做的事是:

  • protocol extension ≈ 命名空间里的 free function
  • 不进 witness table
  • 不参与动态派发
  • 不形成继承 override 关系

所以在多协议场景下:

Swift 并不是在“选最具体的实现”,
而是在“根据静态类型查找一个能用的函数”。


六、工程级解决方案(重点)

✅ 方案 1(最重要):所有“多态点”必须写进 protocol

❌ 错误

extension A {
    func foo() {}
}

✅ 正确

protocol A {
    func foo()
}

✅ 方案 2:子协议“重定义”能力,而不是 extension override

protocol A {
    func foo()
}

protocol B: A {
    func foo()   // 明确重新声明
}

这样:

  • foo() 进入 witness table
  • 多态恢复
  • extension 只提供默认实现

✅ 方案 3:extension 里只写「模板方法」

protocol A {
    func coreFoo()
}

extension A {
    func foo() {
        print("before")
        coreFoo()
        print("after")
    }
}

✔️ foo() 是静态
✔️ 差异在 coreFoo()
✔️ 多协议不会冲突


✅ 方案 4:显式限定协议视角(止血方案)

(x as A).foo()
(x as B).foo()

⚠️ 不是最佳设计
✔️ 但在遗留系统中很实用


✅ 方案 5:避免 extension 中定义“像 override 的方法”

命名区分:

extension A {
    func aFoo()
}

extension B {
    func bFoo()
}

七、团队级“红线规则”(建议直接写进规范)

❌ 禁止在多个 protocol extension 中定义同名、可被误认为 override 的方法
❌ 禁止依赖 extension 方法实现多态
✅ 多态行为必须写进 protocol 本体
✅ extension 只做默认实现 / 工具方法 / 模板方法


八、一句话终极总结(高浓度)

多协议继承下,protocol extension 的静态派发会制造“平行世界的实现”,
如果你把它当成 override,用得越多,系统越不可预测。