在 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 的指针。