# 第11章 协议
协议允许我们进行动态派生,在运行时程序会根据消息接受者的类型去选择正确的方式实现。
普通的协议可以被当作类型约束使用,也可以当作独立的类型使用。带有关联类型或者 Self 约束的协议不能当作独立的类型使用。
在面向对象编程中,子类是在多个类之间共享代码的有效方式,Swift 面向协议的编程,通过协议和协议扩展来实现代码共享。
协议要求的方式是动态派发的,而仅定义在扩展中的方式是静态派发的。
# 类型抹消
# 方法1
- 创建一个名为 AnyProtocolName 的结构体或者类。
- 对于每个关联类型,我们添加一个泛型参数。
- 对于协议的每个方法,我们将其实现存储在 AnyProtocolName 中的一个属性中。
- 我们添加一个将想要抹消的具体类型泛型化的初始化方法,它的任务是在闭包中捕获我们传入的对象,并将闭包赋值给上面步骤中的属性。
// 实现任意迭代器
class AnyIterator<A>: IteratorProtocol {
var nextImpl: () -> A?
init<I: IteratorProtocol>(_ iterator: I) where I.Element == A {
var iteratorCopy = iterator
self.nextImpl = { iteratorCopy.next() }
}
func next() -> A? {
return nextImpl()
}
}
let iter: AnyIterator<Int> = AnyIterator<Int>(ConstantIterator())
2
3
4
5
6
7
8
9
10
11
12
13
14
# 方法2
使用类继承的方式,来把具体的迭代器类型隐藏在子类中,同事面向客户端的类仅仅只是对元素类型的泛型化类型。标准库也是采用这个策略。
// 实现任意迭代器
class IteratorBox<Element>: IteratorProtocol {
func next() -> Element? {
fatalError("This method is abstract.")
}
}
class IteratorBoxHelper<I: IteratorProtocol>: IteratorBox<I.Element> {
var iterator: I
init(_ iterator: I) {
self.iterator = iterator
}
override func next() -> I.Element? {
return iterator.next()
}
}
let iter: IteratorBox<Int> = IteratorBoxHelper(ConstantIterator())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 带有 Self 的协议
== 运算符被定义为了类型的静态函数。换句话说,它不是成员函数,对该函数的调用将被静态派发。
# 协议内幕
当我们通过协议类型创建一个变量的时候,这个变量会被包装到一个叫做存在容器的盒子中
func f<C: CustomStringConvertible>(_ x: C) -> Int {
return MemoryLayout.size(ofValue: x)
}
func g(_ x: CustomStringConvertible) -> Int {
return MemoryLayout.size(ofValue: x)
}
f(5) // 8
g(5) // 40
2
3
4
5
6
7
8
9
因为 f 接受的是泛型参数,整数 5 会被直接传递给这个函数,而不需要经过任何包装。所以它的大小是 8 字节,也就是 64 位系统中 Int 的尺寸。
对于 g,整数会被封装到一个存在容器中。对于普通的协议 (也就是没有被约束为只能由 class 实现的协议),会使用不透明存在容器 (opaque existential container)。不透明存在容器中含有一个存储值的缓冲区 (大小为三个指针,也就是 24 字节);一些元数据 (一个指针,8 字节);以及若干个目击表 (0 个或者多个指针,每个 8 字节)。如果值无法放在缓冲区里,那么它将被存储到堆上,缓冲区里将变为存储引用,它将指向值在堆上的地址。元数据里包含关于类型的信息 (比如是否能够按条件进行类型转换等)。
目击表是让动态派发成为可能的关键。它为一个特定的类型将协议的实现进行编码:对于协议中的每个方法,表中会包含一个指向特定类型中的实现的入口。有时候这被称为 vtable。
如果方法不是协议定义的一部分 (或者说,它不是协议所要求实现的内容,而是扩展方法),所以它也不在目击表中。因此,编译器除了静态地调用协议的默认实现以外,别无选择。
不透明存在容器的尺寸取决于目击表个数的多少,每个协议会对应一个目击表。举例来说, Any 是空协议的类型别名,所以它完全没有目击表
typealias Any = protocol<>
MemoryLayout<Any>.size // 32
2
如果我们合并多个协议,每多加一个协议,就会多 8 字节的数据块。所以合并四个协议将增加 32 字节
protocol Prot { }
protocol Prot2 { }
protocol Prot3 { }
protocol Prot4 { }
typealias P = Prot & Prot2 & Prot3 & Prot4
MemoryLayout<P>.size // 64
2
3
4
5
6
对于只适用于类的协议 (也就是带有 SomeProtocol: class 或者 @objc 声明的协议),会有一个叫做类存在容器的特殊存在容器,这个容器的尺寸只有两个字⻓ (以及每个额外的目击表增加一个字⻓),一个用来存储元数据,另一个 (而不像普通存在容器中的三个) 用来存储指向这个类的一个引用
protocol ClassOnly: AnyObject {}
MemoryLayout<ClassOnly>.size // 16
2
从 Objective-C 导入 Swift 的那些协议不需要额外的元数据。所以那些类型是 Objective-C 协议的变量不需要封装在存在容器中;它们在类型中只包含一个指向它们的类的指针
MemoryLayout<NSObjectProtocol>.size // 8