Protocol Witness in Swift
在 Swift 中,协议是非常重要的部分之一。使用协议,可以让我们写出灵活的代码,而不必拘泥于接口和实现的耦合,但协议也有自己的问题。
问题 1
protocol A {}
protocol B {}
extension B: A {} // 🛑 Extension of protocol 'B' cannot have an inheritance clause
我们无法在协议 B 的扩展中继承协议 A。
protocol A {}
protocol B: A {} // 🟢
只有在协议 B 声明时,才能建立继承关系。当协议 B 非我们自己声明时,那就无计可施了。
问题 2
遵循协议的类型只能以一种方式来实现协议的要求。
indirect enum Tree<A> {
case empty
case node(left: Tree<A>, value: A, right: Tree<A>)
}
// ⚠️ Types are only allowed to conform to a protocol in one single way.
extension Tree: Sequence {
// in-order?
// pre-order?
// post-order?
}
我们利用 Swift 中的 enum 来定义树的结构,并期望 Tree 遵循 Sequence 协议。那么遍历 Tree 时,用哪种方式去遍历是固定的,是根据 IteratorProtocol 协议的实现来定的。
问题 3
有些类型无法遵循协议,如 Tuple。
let tuple1 = (1, 1)
let tuple2 = (2, 2)
@discardableResult
func isEqual<T: Equatable>(lhs: T, rhs: T) -> Bool {
lhs == rhs
}
isEqual(lhs: tuple1, rhs: tuple2) // 🛑 Type '(Int, Int)' cannot conform to 'Equatable'
Protocol Witnesses (协议目击者)
Protocol witnesses are generated by the Swift compiler, when a protocol is used in a generic context, the compiler generates a separate implementation for each concrete type that conforms to the protocols. These implementations called protocol witnesses, are optimized for the specific type.
简而言之,Protocol Witnesses 是 Swift 编译器生成的一种数据结构,这种数据结构里包含了遵循某个协议的实现细节。也就是说,如果 Swift 中没有设计协议,我们也能利用这种数据结构实现协议的特性。
protocol Combinable {
func combine(_ other: Self) -> Self
}
extension Int: Combinable {
func combine(_ other: Int) -> Int {
self + other
}
}
Combinable 是个很简单的协议,我们让 Int 遵循 Combinable 协议,并用整型数的加法来满足协议要求。
如果 Double 和 Float 类型也想变成 Combinable 呢?
😐 嗯,继续添加对应的实现呗!
extension Double: Combinable {
func combine(_ other: Double) -> Double {
self + other
}
}
extension Float: Combinable {
func combine(_ other: Float) -> Float {
self + other
}
}
似乎闻到了坏代码的味道!😷
我们转头一想,Int,Double,Float 都是遵循 Numeric 协议的,那么直接让 Numeric 协议继承自 Combinable,似乎问题就解决了!
// 🛑 Extension of protocol 'Numeric' cannot have an inheritance clause
extension Numeric: Combinable {
}
😇 还是不行!🙅
解决问题 1
让我们写出 Combinable 协议对应的 Witness 来解决这个问题。Combining<A> 是Combinable 协议的 Witness。
struct Combining<A> {
let combine: (A, A) -> A
}
再写出加法的实现:
extension Combining where A: Numeric {
static var sum: Combining {
Combining { $0 + $1 }
}
}
最后,
Combining<Int>.sum.combine(1, 2) // Output: Int 3
Combining<Double>.sum.combine(1.0, 2.0) // Output: Double 3.0
解决问题 2
Int 变成 Combinable,不应该只有加法这一种实现方式,也可以是两个 Int 做乘法。
extension Combining where A: Numeric {
static var sum: Combining {
Combining { $0 + $1 }
}
static var prod: Combining {
Combining { $0 * $1 }
}
}
让我们看一个复杂点的例子:
extension Array where Element: Combinable {
func reduce(_ initial: Element) -> Element {
self.reduce(initial) {
$0.combine($1)
}
}
}
// 转为使用 Combining<A>
extension Array {
func reduce(_ initial: Element, _ combining: Combining<Element>) -> Element {
return reduce(initial, combining.combine)
}
}
[1, 3, 5].reduce(0, .sum) // Output: 9
[CGFloat(2), 3, 5].reduce(1, .prod) // Output: 30
我们只需传入不同实现的 Witness 即可实现同一协议方法的不同实现。
解决问题 3
let tuple1 = (1, 1)
let tuple2 = (2, 2)
@discardableResult
func isEqual<T: Equatable>(lhs: T, rhs: T) -> Bool {
lhs == rhs
}
isEqual(lhs: tuple1, rhs: tuple2) // 🛑 Type '(Int, Int)' cannot conform to 'Equatable'
以 Array 为例,如果 Element 是 Equatable 的,那么整个 Array 也是 Equatable 的。那么同理 (1, 1) 这个 tuple 也应该是 Equatable 或者 Combinable 的,因为整型数 1 是 Equatable 的,也是 Combinable 的!
如果目标是证明: (A, B) 是 Combinable 的,那么我们需要拿出 Combining<(A, B)> 这个 witness 才能证明!但我们现在只有 Combining<A> 和 Combining<B>,能以此为基础推导出 Combining<(A, B)> 吗?
答案是可以:
func zip<A, B>(_ a: Combining<A>, _ b: Combining<B>) -> Combining<(A, B)> {
Combining { lhs, rhs in
(a.combine(lhs.0, rhs.0), b.combine(lhs.1, rhs.1))
}
}
[
(1, 1.1),
(2, 2),
(3, 3),
].reduce((1, 0), zip(.prod, .sum)) // Output: (6, 6.1)
收获
前面的内容讲了不少 Protocol Witness 的内容,但既然 Swift 编译器已经把这些细节隐藏起来了,那我们知道这些有什么用呢?请看下面一个例子:
protocol 动物 {
func 走路()
}
extension 动物 {
func 走路() {
print("我用 4 条腿走路")
}
func 奔跑() {
print("我用 4 条腿奔跑")
}
func 跑跑跳跳() {
走路()
奔跑()
}
}
struct 蜘蛛: 动物 {
func 走路() {
print("我用 8 条腿走路")
}
func 奔跑() {
print("我用 8 条腿奔跑")
}
}
let 小蜘蛛 = 蜘蛛()
小蜘蛛.跑跑跳跳()
// Output:
// 我用 8 条腿走路
// 我用 4 条腿奔跑
我们可以用 Protocol Witness 的知识来说明这个输出的原因。在 Swift 协议中,只有被要求的函数才会进行动态派发,即只有在 Protocol Witness 中的方法才会进行动态派发。
所以,我们可以想象小蜘蛛的 走路 和 奔跑 方法中都有一个隐藏的参数(动物 的 witness)。
在小蜘蛛的 走路 方法执行时,发现 witness 中有 走路 方法的实现,即执行动态派发的方法 走路(输出:”我用 8 条腿走路”)。
在小蜘蛛的 奔跑 方法执行时,发现 witness 中并没有 奔跑 方法的实现,即执行协议扩展中静态派发的方法 奔跑(输出:”我用 4 条腿奔跑”)
很明显,蜘蛛不光用 8 条腿走路,它们还可能是用 8 条腿奔跑的。
那么我们只需把 奔跑 方法加入 动物 协议的 witness 中,即把 奔跑 方法加入 动物 协议中,使其成为协议约束的一部分!这样就可以让这只小蜘蛛也用上 8 条腿奔跑了!
protocol 动物 {
func 走路()
func 奔跑()
}
...
...
let 小蜘蛛 = 蜘蛛()
小蜘蛛.跑跑跳跳()
// Output:
// 我用 8 条腿走路
// 我用 8 条腿奔跑
WWDC 24 中也提到了 Protocol Witness
于 Explore Swift performance 视频 [31: 04] 处:

左边为 DataModel 协议,右边为 DataModel 协议的 Witness。
在每个协议要求的方法中,均有一个额外的隐藏参数——指向 witness 的指针。