协议扩展与面向协议编程
本教程共 93 篇 · 第 68 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:学会用协议扩展给协议要求提供默认实现,理解”面向协议编程”为什么能替代一部分继承的职责。
上一章讲的是协议只定义”要求”,具体实现交给遵循者。但有些行为其实所有遵循者都该一样,没必要让每个类型各写一遍。Swift 的解法是:给协议本身写扩展,在扩展里把方法、计算属性、初始化器、下标直接实现出来。这样所有遵循者自动白拿到这些功能。
1-1 给协议加”自带行为”
用 extension 给协议加实现,遵循者无需任何额外代码就获得了这些方法。下面给 RandomNumberGenerator 加一个 randomBool():
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
let generator = LinearCongruentialGenerator()
print(generator.randomBool())
LinearCongruentialGenerator 自己一行没加,但因为遵循了协议、协议扩展又提供了实现,它直接就能调 randomBool()。这就是协议扩展的核心价值:行为写在协议这一层,而不是散落在每个类型里。
Note协议扩展能给遵循者加实现,但不能让一个协议去”继承”另一个协议——协议继承只能在协议声明里用冒号写。扩展只负责”补实现”,不负责”加要求”。
1-2 默认实现:要求也能有现成答案
协议扩展还能给”协议要求的方法或计算属性”提供默认实现。如果某个遵循者自己写了实现,就用它自己的;否则用扩展里的默认版。
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
这里 PrettyTextRepresentable 继承 TextRepresentable,它要求一个 prettyTextualDescription。扩展给了个默认实现:直接返回 textualDescription。遵循者若觉得”够用了”,就可以不重写,白白省一段代码。
要注意它和”可选协议要求”不是一回事。可选要求(仅 @objc 协议才有)调用时要用可选链 ?,而带默认实现的要求可以像普通成员一样直接调用,不需要问号。
1-3 给协议扩展加约束
扩展协议时还能加条件:只有当遵循者满足某些约束时,扩展里的方法才可用。约束写在协议名后面,用泛型 where 子句表达。
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
这个扩展给标准库的 Collection 协议加了 allEqual(),但只对”元素遵循 Equatable”的集合生效。因为要比较元素是否相等,所以必须约束元素可比较。不满足条件的集合,调用这个方法会编译报错。
let equalNumbers = [100, 100, 100]
let differentNumbers = [100, 100, 200]
print(equalNumbers.allEqual()) // true
print(differentNumbers.allEqual()) // false
如果同一协议有多条带约束的扩展都提供了同名方法,Swift 会用”约束最具体”的那条。这条规则让你能把不同场景的行为拆成多条扩展,互不干扰。
1-4 面向协议编程的思想
所谓”面向协议编程”(Protocol-Oriented Programming,常缩写为 POP),是把协议当作组织代码的核心工具。传统面向对象里,复用行为主要靠类继承;但 Swift 的值类型(结构体、枚举)不能继承,而它们又无处不在。协议扩展正好补上了这块短板:你定义一个协议描述能力,再用协议扩展把通用实现放进去,然后让结构体、枚举、类都来遵循——三者都能平等地获得这份能力。
它的好处很实在。第一,值类型也能复用行为,不必为了共享功能而被迫改成类。第二,一个类型可以同时遵循多个协议,相当于”多重能力叠加”,比单继承灵活。第三,默认实现减少了样板代码,新类型只要补齐真正不同的部分即可。第四,协议作为类型时,调用方只依赖”能力”而不依赖”具体类型”,耦合更低、更易替换。
Tip经验法则:当你想给一批类型共享行为,先想”它们共同的能力是什么”,把它提炼成协议 + 协议扩展;而不是急着建一个基类让大家都去继承。尤其在 Swift 里,这往往更自然。
1-5 一个组合多个协议的小例子
协议组合能把几份小协议拼成”一次性要求”。比如既要能命名、又要能比较年龄:
protocol Named {
var name: String { get }
}
protocol ComparableByAge {
var age: Int { get }
}
struct User: Named, ComparableByAge {
var name: String
var age: Int
}
func older(_ a: Named & ComparableByAge, _ b: Named & ComparableByAge) -> String {
return a.age >= b.age ? a.name : b.name
}
Named 和 ComparableByAge 各自只管一小块能力,谁需要就遵循谁。函数参数用 Named & ComparableByAge 表示”同时具备两者”。这种”小块能力自由拼接”的风格,正是面向协议编程推崇的组织方式。
1-6 协议扩展与条件遵循
泛型类型有时只有在其内部类型满足某协议时,才能去遵循另一个协议。这时用”条件遵循”:在扩展遵循协议时再追加 where 约束。标准库里 Array 就是这样——只有当元素遵循 TextRepresentable 时,[Element] 才遵循它:
extension Array: TextRepresentable where Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
这说明协议扩展不仅能给已有类型补功能,还能让泛型类型”有条件地”去遵循协议,把能力精确地控制在成立的范围内。
1-7 协议扩展不能做的事
讲完能力,也要认清边界。协议扩展不能让一个协议去继承另一个协议——继承只能写在协议声明本身的冒号后面。扩展也不能给协议加”存储属性”,它只能提供计算属性与方法。更重要的是:如果多个来源(比如类型自身、多个扩展)都提供了同一要求的实现,Swift 有一套明确的优先级来选”用谁的”,通常”离类型更近、约束更具体”的胜出。理解这点能避免”我以为会调这个,实际调了那个”的困惑——当你怀疑默认实现没按预期生效,先用最具体的约束、再检查类型自身有没有自己的实现。
1-8 小结
协议扩展让协议不只是”要求清单”,还能自带实现,所有遵循者自动受益。给协议要求提供默认实现,能省掉重复代码;给扩展加 where 约束,则能把功能精确限定在满足条件的类型上。面向协议编程正是围绕这两点展开:用协议描述能力、用协议扩展提供通用实现,让结构体、枚举、类都能平等地复用行为,而不必依赖类继承。下章我们把视角转到协议里更进阶的”关联类型”。