Swift 协议扩展与面向协议编程
协议只写要求、不给实现,这带来了灵活性,但也意味着每个遵循者都要重复写一遍相同的代码。
协议扩展(Protocol Extension)解决了这个问题:它可以给协议补上方法、计算属性和下标的具体实现。
把共享实现放进协议扩展、让不同类型按需遵循,就是 Swift 常说的面向协议编程(Protocol-Oriented Programming)。
这一篇既讲它的用法,也讲它最容易踩的坑。
协议扩展提供默认实现
用 extension 协议名 给协议添加成员,写法和扩展普通类型一样。
扩展里的实现会作为默认实现,遵循者不写也能直接用。
实例
protocol RunoobGreetable {
var name: String { get }
func greet() -> String
}
// 协议扩展提供默认实现
extension RunoobGreetable {
func greet() -> String {
return "你好,我是 \(name)"
}
}
struct RunoobGuest: RunoobGreetable {
var name: String
// 不写 greet(),直接使用默认实现
}
struct RunoobVip: RunoobGreetable {
var name: String
// 提供自己的实现,覆盖默认实现
func greet() -> String {
return "贵宾 \(name),欢迎回来"
}
}
print(RunoobGuest(name: "RUNOOB").greet())
print(RunoobVip(name: "小明").greet())
运行结果:
你好,我是 RUNOOB 贵宾 小明,欢迎回来
RunoobGuest 只提供了 name,greet() 由扩展兜底;RunoobVip 自己写了 greet(),就用它自己的。
这样一来,协议要求「必须有 greet()」,而扩展把「不写就送你一个」也一起解决了。
注意:默认实现让协议要求变成「可选实现」,这是有代价的。如果某个方法本应强制每个类型给出符合自身语义的实现,就不要给它默认实现,否则容易留下一个语义不合适的兜底。
条件符合
协议扩展还能给泛型类型补上「有条件的遵循」:只有当泛型参数满足某个条件时,这个类型才遵循协议。
语法是 extension 类型: 协议 where 条件。
实例
protocol RunoobPayable {
var amount: Double { get }
func summary() -> String
}
struct RunoobOrder: RunoobPayable {
var amount: Double
func summary() -> String {
return "订单 \(amount) 元"
}
}
// 条件符合:只有当 Element 本身是 RunoobPayable 时,数组才遵循 RunoobPayable
extension Array: RunoobPayable where Element: RunoobPayable {
var amount: Double {
return reduce(0) { $0 + $1.amount }
}
func summary() -> String {
return "共 \(count) 笔,合计 \(amount) 元"
}
}
let orders = [RunoobOrder(amount: 19.5), RunoobOrder(amount: 30.5)]
print(orders.summary())
运行结果:
共 2 笔,合计 50.0 元
条件是 Element: RunoobPayable,所以只有元素本身可结算的数组才能调用 summary()。
如果数组元素是 Int,条件不成立,[Int] 就不遵循 RunoobPayable,调用会编译报错。
协议扩展也可以加 where Self: 约束,只给满足额外条件的遵循者开放方法。
实例
protocol RunoobScorable {
var score: Int { get }
}
// 扩展带 Self 约束:只有同时是 Equatable 的遵循者才能用这个方法
extension RunoobScorable where Self: Equatable {
func isBetterThan(_ other: Self) -> Bool {
return score > other.score
}
}
struct RunoobPlayer: RunoobScorable, Equatable {
var name: String
var score: Int
}
let a = RunoobPlayer(name: "小明", score: 90)
let b = RunoobPlayer(name: "小红", score: 85)
print(a.isBetterThan(b))
// 协议扩展同样可以扩展系统协议
extension Collection {
// 只在非空时返回首元素
var runoobFirstOrNil: Element? {
return isEmpty ? nil : first
}
}
print([10, 20, 30].runoobFirstOrNil ?? 0)
print([Int]().runoobFirstOrNil ?? 0)
运行结果:
true 10 0
isBetterThan 的参数是 Self,所以只有遵循者本身可比较时才有意义,这就是 where Self: Equatable 的作用。
扩展系统协议给 Collection 加了个 runoobFirstOrNil,数组、集合、字典都能用上这个属性。
协议扩展成员不走动态派发
这是协议扩展最著名的坑,官方文档专门用一个例子提示过它的反直觉行为。
一句话概括:只写在扩展里、没有写进协议本体的成员,不参与动态派发。
| 成员写法 | 通过协议类型调用 | 派发方式 |
|---|---|---|
| 协议本体里声明 + 扩展里给默认实现 | 调用遵循者的实现 | 动态派发 |
| 只在扩展里声明并实现 | 总是调用扩展里的实现 | 静态派发 |
先看会「出乎意料」的那一种:方法只写在扩展里。
实例
protocol RunoobRunner {
var name: String { get }
}
extension RunoobRunner {
// 这个方法只写在扩展里,不是协议要求
func run() -> String {
return "\(name) 在慢跑"
}
}
struct RunoobAthlete: RunoobRunner {
var name: String
func run() -> String {
return "\(name) 在冲刺"
}
}
let athlete = RunoobAthlete(name: "小明")
print(athlete.run()) // 静态类型是 RunoobAthlete,调用自己的实现
let runner: any RunoobRunner = athlete
print(runner.run()) // 静态类型是协议,调用扩展里的实现
运行结果:
小明 在冲刺 小明 在慢跑
同一个对象,athlete.run() 是「冲刺」,runner.run() 却变成「慢跑」。
原因是 run() 不是协议要求,协议的见证表里没有它的条目,所以通过 any RunoobRunner 调用时只能用扩展里那份静态实现。
再看正确写法:只要把 run() 写进协议本体,它就会动态派发。
实例
protocol RunoobRunner2 {
var name: String { get }
func run() -> String // 声明为协议要求
}
extension RunoobRunner2 {
func run() -> String {
return "\(name) 在慢跑"
}
}
struct RunoobSprinter: RunoobRunner2 {
var name: String
func run() -> String {
return "\(name) 在冲刺"
}
}
let sprinter: any RunoobRunner2 = RunoobSprinter(name: "小红")
print(sprinter.run()) // 协议要求走动态派发,调用类型自己的实现
运行结果:
小红 在冲刺
两种写法的差别只在于 func run() -> String 有没有出现在协议本体里,结果却完全不同。
注意:如果这个成员将来可能被遵循者自定义,并且你会通过协议类型来使用它,就一定要把它写进协议本体。写进协议本体的同时仍然可以在扩展里给默认实现,两者并不冲突。
面向协议编程的取舍
面向协议编程的核心思路是「先定义能力,再组合能力」,而不是先搭一条类继承链。
它对值类型特别友好:结构体和枚举不能继承,但可以遵循协议,因此也能共享协议扩展里的实现。
不过它也不是银弹,下面这张表可以帮助你选型。
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 多个类型共享同一套实现,且彼此没有父子关系 | 协议 + 协议扩展 | 结构体和枚举也能参与,不受单继承限制 |
| 存在真正「是一个」的关系,且需要复用父类状态 | 类继承 | 继承天然携带存储属性和构造器链 |
| 只关心能力,不关心具体类型 | 协议作为类型或泛型约束 | 解耦调用方与实现方 |
| 成员需要多态行为,且会通过协议类型调用 | 写进协议本体 | 避免协议扩展的静态派发陷阱 |
| 追求极致性能、调用频繁 | 泛型约束而非 any | 泛型可以静态派发,any 需要装箱和查表 |
协议扩展的另一个好处是可以在不修改原类型的情况下补上能力,比如给 Collection 加一个便捷属性,所有集合类型立刻都能用。
代价是协议层次一旦设计得过深,调用链会变得难以追踪:一个方法到底走的是默认实现还是遵循者的实现,需要回看协议本体才能确定。
实践中的建议是:协议保持小而专注,一个协议只描述一种能力,再用协议组合把它们拼起来。
常见问题
下面整理了协议扩展与面向协议编程的几个常见疑问。
协议扩展里能声明存储属性吗?
不能。扩展整体都不允许添加存储属性,协议扩展也不例外。
需要「存储」时,要么在遵循者类型里声明,要么用计算属性包一层。
默认实现和协议要求冲突时以谁为准?
只要该方法在协议本体里声明过,遵循者自己的实现优先,默认实现只兜底。
如果方法只在扩展里声明,那么通过协议类型调用时永远走扩展里的实现,遵循者的实现会被忽略。
条件符合会对已有类型产生副作用吗?
不会。条件不满足时类型就不遵循该协议,只是不能调用相关成员而已。
但要注意别对同一个类型写出两套互相矛盾的条件符合,编译器会报重复遵循的错误。
协议扩展和普通类型扩展有什么区别?
普通类型扩展给一个具体类型加成员,影响面限于该类型。
协议扩展给所有遵循者加成员,影响面更大,也更容易和遵循者自己的成员重名,使用时要想清楚派发行为。
