现在位置: 首页 > Swift 教程 > 正文

Swift 协议扩展与面向协议编程

协议只写要求、不给实现,这带来了灵活性,但也意味着每个遵循者都要重复写一遍相同的代码。

协议扩展(Protocol Extension)解决了这个问题:它可以给协议补上方法、计算属性和下标的具体实现。

把共享实现放进协议扩展、让不同类型按需遵循,就是 Swift 常说的面向协议编程(Protocol-Oriented Programming)。

这一篇既讲它的用法,也讲它最容易踩的坑。


协议扩展提供默认实现

extension 协议名 给协议添加成员,写法和扩展普通类型一样。

扩展里的实现会作为默认实现,遵循者不写也能直接用。

实例

import Foundation

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 条件

实例

import Foundation

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: 约束,只给满足额外条件的遵循者开放方法。

实例

import Foundation

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,数组、集合、字典都能用上这个属性。


协议扩展成员不走动态派发

这是协议扩展最著名的坑,官方文档专门用一个例子提示过它的反直觉行为。

协议要求走动态派发、协议扩展成员走静态派发:同一个对象通过具体类型变量调用得到「在冲刺」,通过协议变量调用得到「在慢跑」

一句话概括:只写在扩展里、没有写进协议本体的成员,不参与动态派发

成员写法通过协议类型调用派发方式
协议本体里声明 + 扩展里给默认实现调用遵循者的实现动态派发
只在扩展里声明并实现总是调用扩展里的实现静态派发

先看会「出乎意料」的那一种:方法只写在扩展里。

实例

import Foundation

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() 写进协议本体,它就会动态派发。

实例

import Foundation

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 加一个便捷属性,所有集合类型立刻都能用。

代价是协议层次一旦设计得过深,调用链会变得难以追踪:一个方法到底走的是默认实现还是遵循者的实现,需要回看协议本体才能确定。

实践中的建议是:协议保持小而专注,一个协议只描述一种能力,再用协议组合把它们拼起来。


常见问题

下面整理了协议扩展与面向协议编程的几个常见疑问。

协议扩展里能声明存储属性吗?

不能。扩展整体都不允许添加存储属性,协议扩展也不例外。

需要「存储」时,要么在遵循者类型里声明,要么用计算属性包一层。

默认实现和协议要求冲突时以谁为准?

只要该方法在协议本体里声明过,遵循者自己的实现优先,默认实现只兜底。

如果方法只在扩展里声明,那么通过协议类型调用时永远走扩展里的实现,遵循者的实现会被忽略。

条件符合会对已有类型产生副作用吗?

不会。条件不满足时类型就不遵循该协议,只是不能调用相关成员而已。

但要注意别对同一个类型写出两套互相矛盾的条件符合,编译器会报重复遵循的错误。

协议扩展和普通类型扩展有什么区别?

普通类型扩展给一个具体类型加成员,影响面限于该类型。

协议扩展给所有遵循者加成员,影响面更大,也更容易和遵循者自己的成员重名,使用时要想清楚派发行为。