0%

Protocol Witness in Swift

在 Swift 中,协议是非常重要的部分之一。使用协议,可以让我们写出灵活的代码,而不必拘泥于接口和实现的耦合,但协议也有自己的问题。

问题 1

1
2
3
4
5
protocol A {}

protocol B {}

extension B: A {} // 🛑 Extension of protocol 'B' cannot have an inheritance clause

我们无法在协议 B 的扩展中继承协议 A。

1
2
3
protocol A {}

protocol B: A {} // 🟢

只有在协议 B 声明时,才能建立继承关系。当协议 B 非我们自己声明时,那就无计可施了。

问题 2

遵循协议的类型只能以一种方式来实现协议的要求。

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
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 中没有设计协议,我们也能利用这种数据结构实现协议的特性。

1
2
3
4
5
6
7
8
9
protocol Combinable {
func combine(_ other: Self) -> Self
}

extension Int: Combinable {
func combine(_ other: Int) -> Int {
self + other
}
}

Combinable 是个很简单的协议,我们让 Int 遵循 Combinable 协议,并用整型数的加法来满足协议要求。

如果 Doublefloat 类型也想变成 Combinable 呢?

😐 嗯,继续添加对应的实现呗!

1
2
3
4
5
6
7
8
9
10
11
extension Double: Combinable {
func combine(_ other: Double) -> Double {
self + other
}
}

extension Float: Combinable {
func combine(_ other: Float) -> Float {
self + other
}
}

似乎闻到了坏代码的味道味道!😷

我们转头一想,IntDoubleFloat 都是遵循 Numeric 协议的,那么直接让 Numeric 协议继承于 Combinable,似乎问题就解决了!

1
2
3
4
// 🛑 Extension of protocol 'Numeric' cannot have an inheritance clause
extension Numeric: Combinable {

}

😇 还是不行!🙅

解决问题 1

让我们写出 Combinable 协议对应的 Witness 来解决这个问题。Combining<A>Combinable 协议的 Witness。

1
2
3
struct Combining<A> {
let combine: (A, A) -> A
}

再写出加法的实现:

1
2
3
4
5
extension Combining where A: Numeric {
static var sum: Combining {
Combining { $0 + $1 }
}
}

最后,

1
2
3
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 做乘法。

1
2
3
4
5
6
7
8
9
extension Combining where A: Numeric {
static var sum: Combining {
Combining { $0 + $1 }
}

static var prod: Combining {
Combining { $0 * $1 }
}
}

让我们看一个复杂点的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
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)> 吗?

答案是可以:

1
2
3
4
5
6
7
8
9
10
11
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 编译器已经把这些细节隐藏起来了,那我们知道这些有什么用呢?请看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 条腿奔跑了!

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol 动物 {
func 走路()
func 奔跑()
}

...
...

let 小蜘蛛 = 蜘蛛()
小蜘蛛.跑跑跳跳()
// Output:
// 我用 8 条腿走路
// 我用 8 条腿奔跑

WWDC 24 中也提到了 Protocol Witness

Explore Swift performance 视频 [31: 04] 处:

左边为 DataModel 协议,右边为 DataModel 协议的 Witness。

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