一、先给结论(工程经验版)
POP 用在「边界」,Concrete 用在「核心」
- 系统边界:协议优先(解耦、替换、测试)
- 性能核心路径:具体类型 + 泛型 + 静态派发
- UI / 业务层:协议适度,避免“协议滥用”
- 热路径:避免 protocol existential(
any Protocol)
这句话你记住,后面都是展开解释。
二、为什么 POP 会“伤性能”?(说清楚成本)
1️⃣ Protocol Existential = 动态派发
func draw(_ d: any Drawable) {
d.draw()
}
👉 编译器只能走 witness table
👉 无法内联
👉 有额外间接调用
代价:
- 多一次 indirection
- 破坏内联与优化
- 热路径上会被放大
2️⃣ 泛型 + 协议约束 = 静态派发(快)
func draw<T: Drawable>(_ d: T) {
d.draw()
}
👉 编译期就知道类型
👉 可内联
👉 零成本抽象(接近)
📌 这是 Swift 团队真正推荐的 POP 用法
三、一个真实架构案例:日志系统
❌ 天真 POP(性能差 + 过度抽象)
protocol Logger {
func log(_ message: String)
}
struct FileLogger: Logger { ... }
struct ConsoleLogger: Logger { ... }
let logger: any Logger = FileLogger()
for _ in 0..<1_000_000 {
logger.log("hi")
}
🚨 热路径 + existential = 性能雷区
✅ 工程化 POP(边界抽象 + 核心具体)
1️⃣ 抽象边界(系统入口)
protocol Logger {
func log(_ message: String)
}
2️⃣ 核心实现(具体类型)
struct FileLogger: Logger {
func log(_ message: String) { /* fast path */ }
}
3️⃣ 泛型包裹(静态派发)
struct LogService<L: Logger> {
let logger: L
func log(_ message: String) {
logger.log(message)
}
}
4️⃣ 使用
let service = LogService(logger: FileLogger())
service.log("hi") // 静态派发
📈 性能接近直接调用
📦 抽象仍然存在
🧪 测试可替换
四、设计决策表(你可以直接照这个做)
| 场景 | 推荐方式 |
|---|---|
| 模块边界 | 协议 |
| 核心算法 | struct / enum |
| 热路径 | 泛型约束 |
| 插件系统 | existential |
| 单元测试 | 协议 |
| UI 层 | 协议 + class |
| 库 / Framework | 协议 + 默认实现 |
五、Swift 团队的真实用法(非常关键)
标准库的套路:
extension Array: RandomAccessCollection {}
- 协议定义能力
- 具体类型实现
- 泛型算法使用协议
func quickSort<C: RandomAccessCollection>(_ c: C) { ... }
❌ 他们几乎不会写:
func quickSort(_ c: any RandomAccessCollection)
这就是答案。
六、什么时候该“放弃 POP”?
🚫 明确不适合 POP 的地方
- 数值计算 / 图形 / 音频
- tight loop
- 高频 IO
- 编解码核心逻辑
👉 用 具体类型 + 值语义
七、一个黄金判断问题(超实用)
每次你想加一个协议,问自己一句话:
“我真的需要在运行时替换这个实现吗?”
- ❌ 不需要 → 泛型 / concrete type
- ✅ 需要 → protocol + existential
- 🤔 未来可能 → 协议 + 泛型封装
八、最终口诀(送你一个)
协议用于「解耦」
泛型用于「性能」
existential 用于「灵活性」
具体类型用于「极致速度」
这四个你能分清,Swift 架构就已经是高段位了。