在本主题的继续中,我们将研究协议类型和通用代码。
在此过程中将考虑以下问题:
- 在没有继承和引用类型的情况下实现多态
- 协议类型对象如何存储和使用
- 方法分派如何与他们一起工作
协议类型
在没有继承和引用类型的情况下实现多态:
protocol Drawable { func draw() } struct Point: Drawable { var x, y: Int func draw() { ... } } struct Line: Drawable { var x1, x2, y1, y2: Int func draw() { ... } } var drawbles = [Drawable]() for d in drawbles { d.draw() }
- 表示Drawable协议,它具有draw方法。
- 我们为点和线实现了此协议-现在您可以像使用Drawable一样处理它们(调用draw方法)
我们仍然有一个多态代码。 drawables数组的d元素具有一个接口,该接口在Drawable协议中指示,但具有不同的方法实现,在Line和Point中指示。
多态性的主要原理(即席):“通用接口-许多实现”
没有虚拟表的动态调度
回想一下,使用类(引用类型)时,方法的正确实现的定义是通过动态提交和虚拟表实现的。 每个类类型都有一个虚拟表;它存储其方法的实现。 动态分派通过查看其虚拟表来定义类型的方法实现。 由于继承和覆盖方法的可能性,所有这些都是必要的。
在结构的情况下,继承以及方法的重新定义都是不可能的。 然后,乍一看,不需要虚拟表,但是动态调度又将如何工作呢? 程序如何理解将在d.draw()上调用哪个方法?
值得注意的是,此方法的实现数量等于符合Drawable协议的类型的数量。
协议见证表
是这个问题的答案。 实现协议的每种类型都有此表。 就像类的虚拟表一样,它存储协议所需的方法的实现。
在下文中,协议见证表将被称为“协议方法表”
好的,现在我们知道了在哪里寻找方法的实现。 仅剩下两个问题:
- 如何为实现此协议的对象找到合适的协议方法表? 在我们的情况下,如何为drawables数组的d元素找到此表?
- 数组的元素必须具有相同的大小(这是数组的本质)。 如果可绘制数组可以在其中存储线和点,并且它们具有不同的大小,那么如何满足此要求呢?
MemoryLayout.size(ofValue: Line(...))
现有容器
为了解决这两个问题,Swift使用一种特殊的存储方案存储协议类型的实例,称为存在容器。 看起来像这样:

它需要5个机器字(在x64位系统中为5 * 8 = 40位)。 它分为三个部分:
值缓冲区-实例本身的空间
vwt-指向价值见证表的指针
pwt-指向协议见证表的指针
更详细地考虑所有三个部分:
内容缓冲区
只需三个机器字即可存储实例。 如果实例可以容纳在内容缓冲区中,则将其存储在其中。 如果实例具有3个以上的机器字,则它将不适合缓冲区,并且程序将被迫在堆上分配内存,将该实例放在该内存中,然后将指向该内存的指针放在内容缓冲区中。 考虑一个例子:
let point: Drawable = Point(...)
Point()占用2个机器字,并且完全适合值缓冲区-程序会将其放置在其中:

let line: Drawable = Line(...)
Line()占用4个机器字,无法容纳在值缓冲区中-程序将为其分配内存以供堆使用,并在值缓冲区中添加指向该内存的指针:

ptr指向放置在堆上的Line()的实例:

生命周期表
与协议方法表一样,具有协议的每个表都具有该表。 它包含四个方法的实现:分配,复制,销毁,取消分配。 这些方法控制对象的整个生命周期。 考虑一个例子:
- 创建对象时(点(...)为Drawable),从T.Zh开始分配方法。 这个对象。 分配方法将决定对象的内容应放置在何处(在值缓冲区中还是在堆中),如果应将其放置在堆中,它将分配所需的内存量
- 复制方法会将对象的内容放在适当的位置。
- 处理完对象后,将调用destruct方法,该方法将减少所有链接数(如果有)
- 销毁后,将调用deallocate方法,该方法将释放在堆上分配的内存(如果有)
协议方法表
如上所述,它包含该表绑定到的类型的协议所需的方法的实现。
现有容器-答案
因此,我们回答了两个问题:
- 协议方法表存储在此对象的Existential容器中,可以从中轻松获得
- 如果数组的元素类型是协议,则此数组的任何元素都采用5个机器字的固定值-这正是现有容器所需要的。 如果元素的内容不能放置在值缓冲区中,则它将放置在堆中。 如果可以,那么所有内容将被放置在值缓冲区中。 无论如何,我们得到具有协议类型的对象的大小是5个机器字(40位),并且随之而来的是数组的所有元素都将具有相同的大小。
let line: Drawable = Line(...) MemoryLayout.size(ofValue: line)
现有容器-示例
考虑以下代码中存在容器的行为:
func drawACopy(local: Drawable) { local.draw() } let val: Drawable = Line(...) drawACopy(val)
存在容器可以这样表示:
struct ExistContDrawable { var valueBuffer: (Int, Int, Int) var vwt: ValueWitnessTable var pwt: ProtocolWitnessTable }
伪代码
在幕后,drawACopy函数采用ExistContDrawable:
func drawACopy(val: ExistContDrawable) { ... }
函数参数是手动创建的:创建一个容器,并从接收到的参数中填充其字段:
func drawACopy(val: ExistContDrawable) { var local = ExistContDrawable() let vwt = val.vwt let pwt = val.pwt local.type = type local.pwt = pwt ... }
我们决定内容的存储位置(在缓冲区或堆中)。 我们调用vwt.allocate和vwt.copy来用val填充本地内容:
func drawACopy(val: ExistContDrawable) { ... vwt.allocateBufferAndCopy(&local, val) }
我们调用draw方法并向其传递一个指向self的指针(projectBuffer方法将确定self的位置-在缓冲区中还是在堆上-并返回正确的指针):
func drawACopy(val: ExistContDrawable) { ... pwt.draw(vwt.projectBuffer(&local)) }
我们完成了与当地的合作。 我们从当地清理所有臀部链接。 该函数返回一个值-我们清除分配给drawACopy(堆栈帧)的所有内存:
func drawACopy(val: ExistContDrawable) { ... vwt.destructAndDeallocateBuffer(&local) }
现有容器-目的
使用现有的容器需要大量工作-上面的示例证实了这一点-但是为什么它甚至有必要,目的是什么? 目标是使用协议及其实现类型来实现多态。 在OOP中,我们使用抽象类并通过重写方法从它们继承。 在EPP中,我们使用协议并实现其要求。 同样,即使使用协议,实现多态也是一项艰巨且耗能的工作。 因此,为了避免“不必要的”工作,您需要了解何时需要多态性,何时不需要多态性。
在EPP的实现中,多态性的一个事实是,使用结构,我们不需要常量引用计数,就没有类继承。 是的,一切都非常相似,类使用虚拟表来确定方法的实现,协议使用协议方法。 将类放置在堆上,有时也可以将结构放置在堆上。 但是问题在于,可以将尽可能多的指针指向放置在堆上的类,并且引用计数是必需的,并且只有一个指向放置在堆上的结构的指针,并将其存储在一个存在容器中。
实际上,重要的是要注意,存储在存在性容器中的结构将保留值类型的语义,而不管它是放在堆栈还是堆上。 生命周期表负责语义的保留,因为它描述了确定语义的方法。
现有容器-存储的属性
我们研究了函数如何传递和使用协议类型的变量。 让我们考虑一下如何存储这些变量:
struct Pair { init(_ f: Drawable, _ s: Drawable) { first = f second = s } var first: Drawable var second: Drawable } var pair = Pair(Line(), Point())
这两个Drawable结构如何存储在Pair结构中? 对的内容是什么? 它由两个存在的容器组成-一个用于第一个,另一个用于第二个。 行不能容纳在缓冲区中,而是放置在堆上。 点拟合在缓冲区中。 它还允许Pair结构存储不同大小的对象:
pair.second = Line()
现在,第二个内容也放置在堆上,因为它不适合缓冲区。 考虑一下这可能导致:
let aLine = Line(...) let pair = Pair(aLine, aLine) let copy = pair
执行此代码后,程序将收到以下内存状态:

我们在堆上有4个内存分配,这不好。 让我们尝试修复:
- 创建一个模拟类Line
class LineStorage: Drawable { var x1, y1, x2, y2: Double func draw() {} }
- 我们成对使用
let lineStorage = LineStorage(...) let pair = Pair(lineStorage, lineStorage) let copy = pair
我们在堆上得到一个放置,并指向它四个指针:

但是我们正在处理参照行为。 更改copy.first会影响pair.first(与.second相同),这并不总是我们想要的。
间接存储和更改时复制(写时复制)
在此之前,曾提到过String是写时复制结构(将其内容存储在堆中,并在更改时进行复制)。 考虑如何实现您的结构,该结构在更改时将被复制:
struct BetterLine: Drawable { private var storage: LineStorage init() { storage = LineStorage((0, 0), (10, 10)) } func draw() -> Double { ... } mutating func move() { if !isKnownUniquelyReferenced(&storage) { storage = LineStorage(self.storage) }
- BetterLine将所有属性存储在存储中,而存储是一个类,存储在堆中。
- 只能使用move方法更改存储。 在其中,我们检查是否只有一个指针指向存储。 如果有更多的指针,则此BetterLine与某人共享存储,并且为了使BetterLine完全表现为结构,存储必须是独立的-我们会进行复制并在以后使用它。
让我们看看它如何在内存中工作:
let aLine = BetterLine() let pair = Pair(aLine, aLine) let copy = pair copy.second.x1 = 3.0
通过执行此代码,我们得到:

换句话说,我们有两个Pair实例共享相同的存储:LineStorage。 在一个用户(第一位/第二位)中更改存储时,将为此用户创建一个单独的存储副本,以使其更改不会影响其他用户。 这解决了前面示例中违反值类型语义的问题。
协议类型-摘要
- 小值 。 如果我们使用的对象占用很少的内存并且可以放置在存在容器的缓冲区中,则:
- 堆上没有放置
- 没有参考计数
- 使用协议表进行多态性(动态发送)
- 物超所值。 如果我们使用缓冲区中不适合的对象,则:
已经证明了使用重写进行更改和间接存储的机制,并且在有大量引用引用计数的情况下,可以大大改善这种情况。
我们发现协议类型(如类)能够实现多态。 这是通过存储在存在的容器中并使用协议表(生命周期表和协议方法表)来实现的。