在 Swift 中,协议是非常重要的部分之一。使用协议,可以让我们写出灵活的代码,而不必拘泥于接口和实现的耦合,但协议也有自己的问题。
问题 1
1 | protocol A {} |
我们无法在协议 B 的扩展中继承协议 A。
1 | protocol A {} |
只有在协议 B 声明时,才能建立继承关系。当协议 B 非我们自己声明时,那就无计可施了。
问题 2
遵循协议的类型只能以一种方式来实现协议的要求。
1 | indirect enum Tree<A> { |
我们利用 Swift 中的 enum 来定义树的结构,并期望 Tree 遵循 Sequence 协议。那么遍历 Tree 时,用哪种方式去遍历是固定的,是根据 IteratorProtocol 协议的实现来定的。
问题 3
有些类型无法遵循协议,如 Tuple。
1 | let tuple1 = (1, 1) |
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 中没有设计协议,我们也能利用这种数据结构实现协议的特性。
1 | protocol Combinable { |
Combinable 是个很简单的协议,我们让 Int 遵循 Combinable 协议,并用整型数的加法来满足协议要求。
如果 Double 和 float 类型也想变成 Combinable 呢?
😐 嗯,继续添加对应的实现呗!
1 | extension Double: Combinable { |
似乎闻到了坏代码的味道味道!😷
我们转头一想,Int,Double,Float 都是遵循 Numeric 协议的,那么直接让 Numeric 协议继承于 Combinable,似乎问题就解决了!
1 | // 🛑 Extension of protocol 'Numeric' cannot have an inheritance clause |
😇 还是不行!🙅
解决问题 1
让我们写出 Combinable 协议对应的 Witness 来解决这个问题。Combining<A> 是Combinable 协议的 Witness。
1 | struct Combining<A> { |
再写出加法的实现:
1 | extension Combining where A: Numeric { |
最后,
1 | Combining<Int>.sum.combine(1, 2) // Output: Int 3 |
解决问题 2
Int 变成 Combinable,不应该只有加法这一种实现方式,也可以是两个 Int 做乘法。
1 | extension Combining where A: Numeric { |
让我们看一个复杂点的例子:
1 | extension Array where Element: Combinable { |
我们只需传入不同实现的 Witness 即可实现同一协议方法的不同实现。
解决问题 3
1 | let tuple1 = (1, 1) |
以 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)> 吗?
答案是可以:
1 | func zip<A, B>(_ a: Combining<A>, _ b: Combining<B>) -> Combining<(A, B)> { |
收获
前面的内容讲了不少 Protocol Witness 的内容,但既然 Swift 编译器已经把这些细节隐藏起来了,那我们知道这些有什么用呢?请看下面一个例子:
1 | protocol 动物 { |
我们可以用 Protocol Witness 的知识来说明这个输出的原因。在 Swift 协议中,只有被要求的函数才会进行动态派发,即只有在 Protocol Witness 中的方法才会进行动态派发。
所以,我们可以想象小蜘蛛的 走路 和 奔跑 方法中都有一个隐藏的参数(动物 的 witness)。
在小蜘蛛的 走路 方法执行时,发现 witness 中有 走路 方法的实现,即执行动态派发的方法 走路(输出:”我用 8 条腿走路”)。
在小蜘蛛的 奔跑 方法执行时,发现 witness 中有没有 奔跑 方法的实现,即执行协议扩展中静态派发的方法 奔跑(输出:”我用 4 条腿奔跑”)
很明显,蜘蛛不光用 8 条腿走路,它们还可能是用 8 条腿奔跑的。
那么我们只需把 奔跑 方法加入 动物 协议的 witness 中,即把 奔跑 方法加入 动物 协议中,使其成为协议约束的一部分!这样就可以让这只小蜘蛛也用上 8 条腿奔跑了!
1 | protocol 动物 { |
WWDC 24 中也提到了 Protocol Witness
于 Explore Swift performance 视频 [31: 04] 处:
左边为 DataModel 协议,右边为 DataModel 协议的 Witness。
在每个协议要求的方法中,均有一个额外的隐藏参数——指向 witness 的指针。