4-8.【协议导向编程】如何避免 protocol extension 默认实现被静态派发导致的多态问题?

3 阅读2分钟

一、一句话总原则(先记住)

凡是你希望“可以被不同类型定制 / override”的行为,
必须写进 protocol,而不是只写在 protocol extension 里。

这句话几乎能解决 80% 的坑。


二、问题根源回顾(为什么会出问题)

protocol Cache {}

extension Cache {
    func clear() {
        print("default clear")
    }
}

struct MemoryCache: Cache {
    func clear() {
        print("memory clear")
    }
}

let c: Cache = MemoryCache()
c.clear() // ❗ default clear

原因只有一个:

clear() 不是协议要求
👉 不进 witness table
👉 编译期静态绑定 extension 实现


三、7 种工程级避免方案(从强到弱)

下面是你在真实项目中 可以直接用的做法


✅ 方案 1(最推荐):方法必须声明在 protocol 中

这是唯一真正支持多态的方式

protocol Cache {
    func clear()
}

extension Cache {
    func clear() {
        print("default clear")
    }
}

✔️ extension 提供默认实现
✔️ conforming type 可 override
✔️ protocol 调用走 witness table

📌 这是 Swift 团队设计 POP 的“正统用法”


✅ 方案 2:用 @available(*, unavailable) 防误用

如果你不希望别人错误地在 extension 里“覆盖”

protocol Cache {
    func clear()
}

extension Cache {
    @available(*, unavailable)
    func clear() {}
}

强制要求实现者自己写,防踩坑(库作者常用)。


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

可变部分交给 protocol 要求

protocol Cache {
    func removeAll()
}

extension Cache {
    func clear() {
        print("log clear")
        removeAll()
    }
}

✔️ clear() 是静态派发
✔️ 但真正的差异在 removeAll()
✔️ 多态依然成立

📌 这是最优雅的 POP 组合方式之一


✅ 方案 4:明确区分“多态 API”和“辅助 API”

命名 + 文档约束:

protocol Cache {
    func clear()        // 多态
}

extension Cache {
    func clearWithLog() // 工具方法
}

📌 让 extension 方法“不长得像 override 点”


⚠️ 方案 5:用泛型而不是 existential(间接避免)

func clearCache<C: Cache>(_ cache: C) {
    cache.clear()
}

即便有默认实现,泛型上下文仍然可能静态绑定具体实现。

但注意:

  • 不能修复“协议没声明方法”的问题
  • 只是减少 existential 使用

⚠️ 方案 6:用 class + 继承(仅限需要引用语义)

class Cache {
    func clear() {}
}

class MemoryCache: Cache {
    override func clear() {}
}

适用于:

  • UIKit / AppKit
  • 需要 shared mutable state
  • 框架设计

📌 不是 POP 的主流解法,但现实中有效


🚫 方案 7(不要用):靠“记住规则”

“大家记得不要在 extension 里 override 就好”

❌ 不可规模化
❌ 团队必翻车


四、一个“黄金设计模板”(非常实用)

🔒 稳定多态接口

protocol Renderer {
    func render()
}

🧱 默认行为(extension)

extension Renderer {
    func render() {
        prepare()
        draw()
    }

    func prepare() {
        print("default prepare")
    }

    func draw() {
        fatalError("must implement")
    }
}

🎯 自定义点(协议要求)

struct OpenGLRenderer: Renderer {
    func draw() {
        print("OpenGL draw")
    }
}

📌 多态只发生在 明确的协议要求点


五、团队级 Code Review 检查清单(直接用)

每看到一个 protocol extension,问 4 个问题:

  1. 这个方法 需要被 override 吗?
  2. 如果需要,它 是否声明在 protocol 中?
  3. extension 方法是否只是 模板 / 工具 / 派生行为?
  4. 是否有命名 / 文档明确区分?

如果 1️⃣ = YES,2️⃣ = NO → 必改


六、一句话终极总结(刻在脑子里)

extension 里的默认实现是“复用工具”,不是“多态入口”。
多态入口必须写在 protocol 本体中。