Swift 协议探究

190 阅读5分钟

Swift 协议是如何实现的? 为什么能够把协议当作类型, 同时适用于结构体和类?

当我们把协议当作具体类型使用的时候, 编译器会为协议创建一个包装类型, 叫做存在体, 存在体的大小是固定的, 为 32 字节, 如果储存的对象是 struct, struct 的大小小于等于 24 字节并且每个属性的大小不超过 8 字节, 那么就会被储存在存在体的前 24 字节 buffer 中, 否则系统会在堆上额外开辟一块空间来储存该 struct, 并且在存在体的前 8 个字节中储存指向该内存空间的指针, 而 class 由于本身就是一个指针, 也是在存在体前八个字节中储存对象的地址, 而剩下的 16 字节将不会被初始化, 24-32 字节会储存该对象的的具体类型的值目击表 (Value Witness Table ), 包括如何创建, 复制, 销毁该类型的信息, 32 字节以后每个实现的协议会添加 8 个字节的协议目击者(Protocol Witness Table), 比如 Ecodable 类型会增加一个 8 个字节的协议目击者, 而 Codable (aka Encodable & Decodable) 会增加两个协议目击者, 协议目击者包含了该类型对协议的具体实现.

这种实现方式让协议类型的具体方法实现完全在运行时决定, 编译器无法对此进行优化, 会带来额外的创建包装体的性能开销, 不过包装体在目前的 Swift 代码中是实现细节, 我们在代码中无法获得到这个包装体.

验证方法: MemoryLayout.size, lldb 中使用 x ( aka memory read)

为什么协议类型没有实现协议? 

这种类型的错误, 一般发生在使用带有约束的泛型参数的函数上, 初学者可能对这个错误感到很困惑, 因为在直觉上这应该是可行的, eg

func encode<T: Encodable>(_ encodableObj: T) { // doSomethingHere}
let a = 1 as Encodableencode(a) // 编译器报错

为什么这种看起来没有问题的代码也会报错, 本宝宝做错了什么 ???

别急, 让我来给你慢慢分析

从编译器的直接报错来看, 我们给 a 定义的类型是 Encodable 类型, 而 Encodable 类型的对象并不被编译器认为是实现了 Encodable 协议的, 那编译器为什么会做出这种决定呢?

先说个最直观能被理解的例子, 假定我们有以下协议, 并且协议类型实现了协议, 那么

protocol Initializable {
    init()
}

我们可以直接创建一个 Initializable() 的对象, 这显然是不被允许的, 只有实现了 Initializable

协议的 Struct/Enum/Class 可以符合该协议, 使用 Initializable 中的 init 方法.

对于静态方法, 也是同样的道理, 毕竟协议类型自身一定没有实现 static 方法

其实 init 方法也可以被认为是静态方法, 因为它们都不是对实例变量调用的, 是对某个类型调用的

再举一个例子

extension String: Initializable {}extension Date: Initializable {}extension Array where Element: Initializable {    mutating func appendNew() {        append(Element())    }}let initializables = ["123", Date()] as [Initializable]
initializables.appendNew() // 编译器报错: Value of type [Initializable] has no member of appendNew

这里编译器报错的原因也是因为 Initializable 类型没有实现 Initialiable 协议, 所以它没有 appendNew() 方法, 不过这个非常好理解, 我们不可能在不知道具体类型的时候调用 init 方法或者静态方法.

那遇到这种情况我们该怎么办呢?

1. 将带有泛型参数的函数换成使用协议类型的函数, 这样 Swift 编译器会用存在体来包装具体的参数, 具体的类型只有在运行时才能得到, 而使用泛型参数的参数的具体类型需要在编译时就能够确定(参数要求的是一个存在体还是一个符合协议的具体类型很重要), 以便编译器能够对各种类型生成一个特化的版本, 特化版本不需要包装体, 性能更好, 但是会导致更长的编译时间和更大的二进制程序.

2. 如果确实需要泛型参数, 协议可以标记为 @ojbc 的话, 协议中没有 init 和静态方法, 直接将协议标记成 @objc 即可成功编译, 因为在此时它会被编译器认为是自实现的.

3. 对于上面数组的例子来说, 如果确实需要, 可以把 where 后条件改成 extension Array where Element == Initializable, 这里应该使用的是包装体, 但是在这里你不能使用 init / 静态方法, 否则编译器会提示你不能使用.

PS1: 对于没有 init / 静态方法的协议编译器不认为自实现( 除非加上 @objc ) 是 Swift 目前的一个缺陷, Swift Evolution 已经有几个相关的提案并且没有被否决, 应该在未来的某个版本会被予以支持, 但是目前没有时间表, 可能优先级也不是很高, 工作量也未知, 大家目前还是只能用相应的其他解决方案.

PS2: 除了 @objc 协议, 编译器还有个特例, 就是 Error 自身实现了 Error 协议, 所以你可以使用 Error 类型而不需要具体的 Error 来作为泛型, eg: Result<Int, Error>.