5-18.【性能分析与优化】protocol extension 的默认实现为什么会导致“看似多态,实际非多态”的问题?

5 阅读3分钟

1️⃣ 现象示例

protocol P {
    func foo()
}

extension P {
    func foo() {
        print("protocol extension foo")
    }
}

struct S: P {}

let s: S = S()
s.foo()       // 输出: protocol extension foo

let p: P = S()
p.foo()       // 输出: protocol extension foo

为什么看似“多态”却不是?

  • 协议扩展默认实现是在 静态派发(Static Dispatch)
  • 即便 p 类型是协议类型,也不会调用结构体 S 自己的方法(除非 S 显式实现 foo

2️⃣ 原理分析

(1)协议扩展方法是静态分发

  • 扩展方法在编译期解析调用目标
  • 调用点类型决定调用的实现
let s: S = S()
s.foo()  // S 类型已知 → 静态调用 P extension 的实现
  • 不是通过 witness table → 不是动态派发

(2)协议存在类型调用规则

  • 协议类型变量(existential)调用协议要求的方法:

    • 只有 协议本身声明的方法 并且被类型覆盖,才动态派发
    • 如果只是协议扩展提供的默认实现 → 静态派发,不走动态派发
struct S2: P {
    func foo() { print("S2 foo") }
}

let p2: P = S2()
p2.foo()  // 输出: S2 foo(动态派发,因为 S2 覆盖了协议要求方法)
  • 结论:

    • 默认实现不会被协议存在类型的动态派发捕获
    • 只有显式在类型上实现的方法才参与动态派发

(3)多态 vs 静态分发对比

情况调用类型是否动态派发输出
S 没实现 foos: S静态派发protocol extension foo
S 没实现 foop: P静态派发protocol extension foo
S 显式实现 foop: P动态派发S 的实现

核心差异:协议扩展的默认实现只参与静态派发,不参与多态动态派发


3️⃣ 典型问题场景

  1. 多态调用期望失效
func callFoo(_ p: P) {
    p.foo()  // 期望多态,但如果 p 的类型没有覆盖协议方法 → 调用扩展方法(静态派发)
}
  1. 泛型函数与协议约束结合时
func genericCall<T: P>(_ t: T) {
    t.foo()  // 泛型 T 已知 → 静态派发扩展方法
}
  • 即使不同类型调用,仍可能调用同一静态实现 → “非多态”

4️⃣ 如何解决 / 避免

  1. 在类型上显式实现协议方法
struct S: P {
    func foo() { print("S foo") }
}
  • 这样协议存在类型调用会动态派发 → 多态正确
  1. 只用协议扩展方法做默认实现,不依赖多态
protocol P {
    func foo()
}
extension P {
    func foo() { print("default") }
}

// 泛型 T: P 调用 foo → 编译期静态调用
  • 用来提供 默认行为,不是多态行为
  1. 协议继承 + 覆盖设计
  • 如果需要真正多态,可以使用类 + 协议结合,或者显式在 struct/enum 上实现方法

5️⃣ 总结

协议扩展默认实现的“看似多态,实际非多态”问题本质是静态派发

  • 默认实现属于 静态分发,调用点类型决定实现
  • 只有显式在类型上实现协议要求方法,才会走 动态派发(witness table)
  • 泛型函数、协议存在类型调用和默认实现结合时,很容易产生“非多态行为”