使用泛型类型的时候,通常都会使用协议约束泛型参数的行为。
- 通过协议,可以构建一个依赖数字 (而不是诸如 Int,Double 等某个具体的数值类型) 或集合类型的算法。这样一来,所有实现了这个协议的类型就都具备了这个新算法提供的能力。
- 通过协议还可以抽象代码接口背后的实现细节,可以针对协议进行编程,后让不同的类型实现这个协议。例如,一个使用了 Drawable 协议的画图程序既可以使用 SVG 来渲染图形,也可以使用 Core Graphics。类似的,跨平台的代码可以使用一个 Platform 协议,然后由类似 Linux,macOS 或 iOS 这样的类型来提供具体的实现。
- 可以使用协议让代码更具可测试性。更具体地说,当基于协议而不是一个具体类型来实现某个功能的时候,在测试用例中就很容易把这部分替换成表示各种待测试结果的类型。
在 Swift 里,一个协议表示一组正式提出的要求 (requirements)。例如,Equatable 协议要求实现的类型提供 == 操作符。这些要求可以是普通方法、初始化方法、关联类型、属性和继承的协议。有些协议还有一些无法用 Swift 类型系统表达的要求,例如,Collection 协议就要求通过下标操作符访问元素的时间复杂度是O(1)(但你也可以违背这个要求,如果算法的时间复杂度不是 O(1),在方法的文档中明确说明就行了)。
协议可以自行扩展新的功能。最简单的例子就是 Equatable,它要求实现的类型提供 == 操作符。然后,它会根据 == 的实现提供 != 操作符的功能。类似的,Sequence 协议要求的方法并不多 (它只要求提供一个产生迭代器的方法),但它却可以通过扩展,为自己加入大量可供使用的方法。
协议可以通过条件化扩展 (conditional extensions) 添加需要额外约束的 API。例如,在 Collection 协议中,只有 Element 实现了 Comparable 的时候,才提供了 max() 方法。
协议可以继承其它协议。例如,Hashable 要求实现的类型必须同时实现 Equatable 协议。类似的RangeReplaceableCollection 继承自 Collection,而 Collection 继承自 Sequence。换 句话说,我们可以构建一个协议层次结构。
另外,协议还可以被组合起来形成新的协议。例如,标准库中的 Codable 就是 Encodable 和 Decodable 协议组合之后的别名。
有时,某个协议的实现还依赖于其它协议的实现。例如,当且仅当数组中 Element 类型实现了 Equatable 的时候,对应的数组类型才实现了 Equatable。这叫做条件化实现 (conditional conformance):Array 实现 Equatable 的条件,就是 Element 实现了 Equatable。
协议还可以声明关联类型,实现了这个协议的类型就需要定义关联类型对应的具体类型。例如, IteratorProtocol 定义了一个关联类型 Element,每一个实现了 IteratorProtocol 的类型就都要定义自己的 Element 类型。
每一个协议都会引入一层额外的抽象,有时,这会增加理解代码的难度。但有时,使用协议又可以极大地简化代码。
协议目击者
理解协议的工作方式。假设 Swift 中没有协议这个特性,这时,如果要给 Array 添加一个判断元素是否全部相等的方法,没有 Equatable 协议的话,只能给这个方法传递一个用于比较的函数:
extension Array {
func allEqual(_ compare: (Element, Element) -> Bool) -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard compare(f, el) else { return false }
}
return true
}
}
复制代码
为了让事情更正式一些,可以基于 allEqual 的参数创建一个封装,让它更明确的表达相等比较的含义:
struct Eq<A> {
let eq: (A, A) -> Bool
}
复制代码
现在就可以为比较不同的具体类型 (例如:Int) 创建不同的 Eq 实例了。管这些实例叫做表示相等判断的显式目击者 (explicit witnesses):
let eqInt: Eq<Int> = Eq { $0 == $1 }
复制代码
接下来,就可以用 Eq 改造之前的 allEqual 实现了。使用泛型类型 Element 来表达要比较的所有元素的类型:
extension Array {
func allEqual(_ compare: Eq<Element>) -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard compare.eq(f, el) else { return false }
}
return true
}
}
复制代码
尽管 Eq 放在这里看上去有点儿晦涩,但正是它为我们呈现了协议在背后的工作方式:为一个泛型类型添加了 Equatable 约束之后,只要创建一个对应的具体类型的实例,就会有一个协议目击者传递给它。在 Equatable 的例子中,这个目击者携带的,正是用于比较两个值的 == 操作符。基于要创建的具体类型,编译器会自动传入协议目击者。下面,则是通过协议取代了显式目击者 (explicit witnesses) 的 allEqual 实现:
extension Array where Element: Equatable {
func allEqual() -> Bool {
guard let f = first else { return true }
for el in dropFirst() {
guard f == el else { return false }
}
return true
}
}
复制代码
还可以给 Eq 添加一个扩展。例如,只要定义了比较两个元素是否相等的方法,就可以实现一个判断两个元素不等的方法:
extension Eq {
func notEqual(_ l: A, _ r: A) -> Bool {
return !eq(l,r)
}
}
复制代码
这和通过扩展给协议添加功能是类似的:由于 eq 方法是肯定存在的,就能基于这个方法构建更多功能。于是,给 Equatable 添加同样功能的扩展和上面这个 notEqual 的实现,几乎就是一回事。只不过,相比泛型参数 A,可以使用隐式泛型参数 Self,用它表示实现了协议的类型:
extension Equatable {
static func notEqual(_ l: Self, _ r: Self) -> Bool {
return !(l == r)
}
}
复制代码
这正是标准库为 Equatable 实现 != 操作符的方法。
条件化协议实现 (Conditional Conformance)
为了实现比较数组的 Eq,需要一种比较数组中两个元素的方法。这次,把 eqArray 定义成函数,然后把显式目击者传递给它:
func eqArray<El>(_ eqElement: Eq<El>) -> Eq<[El]> {
return Eq { arr1, arr2 in
guard arr1.count == arr2.count else { return false }
for (l, r) in zip(arr1, arr2) {
guard eqElement.eq(l, r) else { return false }
}
return true
}
}
复制代码
eqArray 诠释了 Swift 中条件化协议实现的工作方式。例如,下面是标准库中 Array 对 Equatable 的实现:
extension Array: Equatable where Element: Equatable {
static func ==(lhs: [Element], rhs: [Element]) -> Bool {
fatalError("Implementation left out")
}
}
复制代码
这里,给 Element 添加 Equatable 约束,和之前把 eqElement 传递给 eqArray 函数本质上是 一样的。在 Array 的扩展里,我们就可以直接使用 == 操作符比较两个元素的值了。而这两种 方法最大的区别就是,使用协议约束类型,编译器会自动传递一个协议目击者。
协议继承
Swift 还支持协议的继承。例如,实现 Comparable 的类型也一定实现了 Equatable。这叫做细化 (refining),换句话说,Comparable 改进了 Equatable:
public protocol Comparable : Equatable {
static func < (lhs: Self, rhs: Self) -> Bool // ...
}
复制代码
在之前假想的没有协议特性的 Swift 版本里,我们也可以表达这种协议细化的想法。为此,先为 Comparable 创建一个显式目击者,让它包含 Equatable 的目击者和一个 lessThan 函数:
struct Comp<A> {
let equatable: Eq<A>
let lessThan: (A, A) -> Bool
}
复制代码
这次,Comp 的定义向我们诠释了一个从其它协议继承而来的新协议的目击者的工作方式。这样,在 Comp 的扩展里,我们就可以使用 Eq 和 lessThan 了:
extension Comp {
func greaterThanOrEqual(_ l: A, _ r: A) -> Bool {
return lessThan(r, l) || equatable.eq(l, r)
}
}
复制代码
这种传递显式目击者的模式对于我们编译器内部对协议的支持很有帮助。而这,也有助于在用协议解决问题卡壳的时候,帮我们找到思路。
但是,(显式传递目击者和使用协议约束类型)这两种做法并不完全相同。同一种类型可以有无数多个显式目击者,但一个类型只能对协议约束的方法提供一份实现。并且,不像显式目击者可以通过参数手动传递,协议目击者的传递是自动的。
如果允许为一个协议提供多份实现,编译器就需要一些方法找到当前环境里最合适的实现。如果这个过程再加上条件化协议实现,就会更加复杂。为了避免这种复杂性,Swift 不允许我们这样做。
使用协议进行设计
看个绘图协议的例子。有两个具体类型会实现这个协议:可以把图形绘制成 SVG 或渲染到 Apple 自家 Core Graphics 框架的图形上下文 (graphics context) 里。从定义一个要求实现绘制椭圆和矩形接口的协议开始:
protocol DrawingContext {
mutating func addEllipse(rect: CGRect, fill: UIColor)
mutating func addRectangle(rect: CGRect, fill: UIColor)
}
复制代码
让 CGContext 实现这个协议:
extension CGContext: DrawingContext {
func addEllipse(rect: CGRect, fill fillColor: UIColor) {
setFillColor(fillColor.cgColor)
fillEllipse(in: rect)
}
func addRectangle(rect: CGRect, fill fillColor: UIColor) {
setFillColor(fillColor.cgColor)
fill(rect)
}
}
复制代码
类似的,让 SVG 实现这个协议.把矩形转换成一系列 XML 属性,并把 UIColor 转换成一个十六进制字符串:
extension SVG: DrawingContext {
mutating func addEllipse(rect: CGRect, fill: UIColor) {
var attributes: [String:String] = rect.svgEllipseAttributes
attributes["fill"] = String(hexColor: fill)
append(Node(tag: "ellipse", attributes: attributes))
}
mutating func addRectangle(rect: CGRect, fill: UIColor) {
var attributes: [String:String] = rect.svgAttributes
attributes["fill"] = String(hexColor: fill)
append(Node(tag: "rect", attributes: attributes))
}
}
复制代码
协议扩展
Swift 协议中的一个关键特性就是协议扩展 (protocol extension)。只要知道了如何绘制椭圆,就可以添加一个扩展来以某点为圆心绘制圆形。例如,给 DrawingContext 添加下面这样的扩展:
extension DrawingContext {
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
let diameter = radius * 2
let origin = CGPoint(x: center.x - radius, y: center.y - radius)
let size = CGSize(width: diameter, height: diameter)
let rect = CGRect(origin: origin, size: size)
addEllipse(rect: rect.integral, fill: fill)
}
}
复制代码
为了使用它,可以给 DrawingContext 再创建一个扩展,给它添加一个在黄色方块中绘制蓝色圆形的方法:
extension DrawingContext {
mutating func drawSomething() {
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
addRectangle(rect: rect, fill: .yellow)
let center = CGPoint(x: rect.midX, y: rect.midY)
addCircle(center: center, radius: 25, fill: .blue)
}
}
复制代码
把这个方法定义在 DrawingContext 的扩展里,就能通过 SVG 或 CGContext 实例调用它。这是一种贯穿 Swift 标准库实现的做法:只要你实现协议要求的几个少数方法,就可以 “免费” 收获这个协议通过扩展得到的所有功能。
定制协议扩展
通过扩展给协议添加的方法,并不作为协议约束的一部分。在某些情况下,这会导致出乎意料的结果。回到之前的例子中,希望使用 SVG 对圆形内建的支持,也就是说:圆形在 SVG 中应该就按照圆形的方式,而不是椭圆的方式进行绘制。于是,在 SVG 的实现里,我们添加了一 个 addCircle 方法:
extension SVG {
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
let attributes = [
"cx": "\(center.x)",
"cy": "\(center.y)",
"r": "\(radius)",
"fill": String(hexColor: fill),
]
append(Node(tag: "circle", attributes: attributes)) }
}
复制代码
当创建一个 SVG 变量并调用 addCircle 方法的时候,它的表现和预期是一样的:
var circle = SVG()
circle.addCircle(center: .zero, radius: 20, fill: .red) circle
/*
<svg>
<circle cx="0.0" cy="0.0" fill="#ff0000" r="20.0"/> </svg>
*/
复制代码
但是,当我们调用定义在 Drawing 上的 drawSomething() (这个方法里有调用 addCircle) 时, 为 SVG 扩展的 addCircle 并不会被调用。在下面的结果里可以看到,SVG 语法中包含的是 ellipse 标签而不是我们期望的 circle:
var drawing = SVG() drawing.drawSomething() drawing
/*
<svg>
<rect fill="#ffff00" height="100.0" width="100.0" x="0.0" y="0.0"/>
<ellipse cx="50.0" cy="50.0" fill="#0000ff" rx="25.0" ry="25.0"/>
</svg>
*/
复制代码
和实现协议约束的方法对比,这种行为让人惊讶。为了了解发生了什么,先把 drawSomething 写成一个泛型全局函数。它表达的语意和协议扩展中的实现是完全一样的:
func drawSomething<D: DrawingContext>(context: inout D) {
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.addRectangle(rect: rect, fill: .yellow)
let center = CGPoint(x: rect.midX, y: rect.midY)
context.addCircle(center: center, radius: 25, fill: .blue)
}
复制代码
这里,泛型参数 D 是一个实现了 DrawingContext 的类型。这就意味着调用 drawSomething 的时候,编译期就会自动传递一个 DrawingContext 的协议目击者。这个目击者只带有协议约束的所有方法,也就是 addRectangle 和 addEllipse。由于 addCircle 仅是一个定义在扩展里的方法,它并不是这个协议约束的一部分,因此也就不在目击者里了。
这个问题的关键就是只有协议目击者中的方法才能被动态派发到一个具体类型对应的实现,因为只有目击者中的信息在运行时是可用的。在泛型上下文环境中,调用协议中的非约束方法总是会被静态派发到协议扩展中的实现。
结果就是,当从 drawSomething 中调用 addCircle 的时候,调用总是会静态派发到协议扩展中的实现。编译器无法生成必要的动态派发的代码去调用我们给 SVG 扩展中添加的实现。为了获得动态派发的行为,我们应该让 addCircle 成为协议约束的一部分:
protocol DrawingContext {
mutating func addEllipse(rect: CGRect, fill: UIColor)
mutating func addRectangle(rect: CGRect, fill: UIColor)
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor)
}
复制代码
这样,在协议扩展中 addCircle 的实现就变成了协议约束的默认实现。有了这个默认实现,之前实现了 DrawingContext 的代码无需任何修改,仍旧可以通过编译。现在,addCircle 成了协议的一部分之后,它也就成为了协议目击者中的一员,当我们再调用 SVG 对象的 drawSomething 方法时,就会调用到预期的 addCircle 实现了:
var drawing2 = SVG() drawing2.drawSomething() drawing2
/*
<svg>
<rect fill="#ffff00" height="100.0" width="100.0" x="0.0" y="0.0"/>
<circle cx="50.0" cy="50.0" fill="#0000ff" r="25.0"/>
</svg>
*/
复制代码
带有默认实现的协议方法在 Swift 社区中有时也叫做定制点 (customization point)。实现协议的类型会收到一份方法的默认实现,并有权决定是否要对其进行覆盖。标准库中这种定制点随处可见。一个例子,就是计算集合中两个元素之间距离的 distance(from:to:) 。这个方法默认实现的时间复杂度是 O(n),因为它要遍历两个元素之间的所有位置。由于 distance(from:to:)也是一个定制点,对于那些可以提供更有效率实现的类型,例如 Array,就可以重写默认的实现了。
协议组合
协议可以被组合在一起。标准库中的一个例子,就是 Codable,它是 Encodable & Decodable 这种形式的别名:
typealias Codable = Decodable & Encodable
复制代码
这就意味着编写下面的函数,我们就能在它的实现里,通过 value 同时使用这两个协议约束的
方法了:
func useCodable<C: Codable>(value: C) {
// ...
}
复制代码
可以把这种 Encodable & Decodable 组合理解成一个新的协议,并且用别名定义成了 Codable。
在之前绘图的例子中,可能希望渲染一些带有属性的字符串 (这些字符串会包含一些表示格式的子区间,例如:粗体、字体和颜色等)。但是,SVG 并没有提供属性字符串的原生支持 (Core Graphics 是可以的)。相比给 DrawingContext 添加一个新的方法,我们创建了一个新的协议:
protocol AttributedDrawingContext {
mutating func draw(_ str: NSAttributedString, at: CGPoint)
}
复制代码
这样,就可以只让 CGContext 实现这个协议,而无需给 SVG 添加同样的支持。并且,还可以把这两个协议合在一起。例如,给 DrawContext 添加一个扩展,要求实现它的类型同样实现了 AttributedDrawingContext:
extension DrawingContext where Self: AttributedDrawingContext {
mutating func drawSomething2() {
let size = CGSize(width: 200, height: 100)
addRectangle(rect: .init(origin: .zero, size: size), fill: .red)
draw(NSAttributedString(string: "hello"), at: CGPoint(x: 50,y: 50))
}
}
复制代码
或者,也可以写一个带有泛型参数约束的函数。这个函数和扩展中的方法语意上是一样的:
func drawSomething2<C: DrawingContext & AttributedDrawingContext>( _ c: inout C)
{
// ...
}
复制代码
协议组合是非常强大的语法工具,通过它,可以给协议添加一些不是所有实现了该协议的类型都支持的操作。
协议继承
除了像上一节那样把协议组合起来,协议之间还可以是继承关系。例如,之前定义的 AttributedDrawingContext 还可以写成这样:
protocol AttributedDrawingContext: DrawingContext {
mutating func draw(_ str: NSAttributedString, at: CGPoint)
}
复制代码
这个定义就要求实现了 AttributedDrawingContext 的类型,必须同时实现 DrawingContext。
协议继承和协议组合有它们各自的应用场景。例如:Comparable 协议就继承自 Equatable。这意味着我们只要让实现 Comparable 的类型实现 < 操作符,它就可以自动添加诸如 >= 和 <= 操作符的定义了。而在 Codable 的例子中,让 Encodable 继承自 Decodable,或者反之,都是没道理的。但是,定义一个叫做 Codable 的新协议,让它同时继承自 Encodable 和 Decodable 则完全没问题。
实际上,typealias Codable = Encodable & Decodable 这种写法 在语法上,和 protocol Codable: Encodable, Decodable {} 是完全一样的。只是别名的写法看上去稍微简洁了一点,它更明确地告诉我们:Codable 仅仅是这两个协议的组合,并没有在组 合的结果里添加任何新的方法。
协议和关联类型
有些协议需要约束的不仅仅是方法、属性和初始化方法,它们还希望和它相关的一些类型满足特定的条件。这就可以通过关联类型 (associated type) 来实现。
在我们自己的代码里,关联类型并不常用,但标准库中却随处可见。其中,一个最简短的例子就是标准库中的 IteratorProtocol 协议。它有一个关联类型表示迭代的元素,以及一个访问下个元素的方法:
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
复制代码
Collection 协议有五个关联类型,它们之中大多都有默认值。例如,关联类型 SubSequence 的默认值是 Slice<Self>
。当然一个类型实现 Collection 的时候,这是另外一个定制点:可以选择使用默认实现来减少开发的工作量。而那些为了性能或使用更加方便的集合类型,通常也会覆盖这个类型 (例如:String 使用 Substring 作为 SubSequence 类型)。
通过协议关联类型,重新实现一个小型的 UIKit 状态恢复机制。在 UIKit 里,状态恢复需要读取视图控制器以及视图的架构,并在 app 挂起的时候将它们的状态序列化。当 App 下一次加载的时候,UIKit 会尝试恢复应用程序的状态。
接下来,我们将使用协议,而不是一个类继承结构,来表示视图控制器。在真实的实现中, ViewController 协议可能会包含很多方法,但为了简单起见,我们让它是一个空的协议:
protocol ViewController {}
复制代码
为了恢复一个特定的视图控制器,需要能够读写它的状态,还希望这个状态实现了 Codable 以便进行编码和解码。由于这个状态和具体的视图控制器相关,它就可以定义成一个关联类型:
protocol Restorable {
associatedtype State: Codable
var state: State { get set }
}
复制代码
为了演示,创建一个显示消息的视图控制器。这个视图控制器的状态由一个消息数组以及当前的滚动位置构成,我们把它定义成一个实现了 Codable 的内嵌类型:
class MessagesVC: ViewController, Restorable {
typealias State = MessagesState
struct MessagesState: Codable {
var messages: [String] = []
var scrollPosition: CGFloat = 0
}
var state: MessagesState = MessagesState()
}
复制代码
实际上,在实现 Restorable 的代码里,无需声明 typealias State。编译器足够聪明,它可以通过 state 属性推断出 State 的类型。也可以把 MessagesState 重命名成 State,一切仍旧可以正常工作。
基于关联类型的条件化协议实现
有些类型只在特定条件下才会实现一个协议。就像之前在条件化协议实现这一节中看到的,只有当数组中元素的类型实现了 Equatable 的时候,Array 才是个 Equatable 的类型。在约束协议实现的条件中,也可以使用关联类型的信息。例如,Range 有一个泛型参数 Bound。当且仅当 Bound 实现了 Strideable 协议,并且 Bound 中的 Stride (这是 Strideable 的一个关联类型) 是一个实现了 SignedInteger 协议的时候,Range 才是一个实现了 Sequence 的类型:
extension Range: Sequence
where Bound: Strideable, Bound.Stride: SignedInteger
复制代码
SplitViewController,它用两个泛型参数表示它的两 个子视图控制器:
class SplitViewController<Master: ViewController, Detail: ViewController> {
var master: Master
var detail: Detail
init(master: Master, detail: Detail) {
self.master = master
self.detail = detail
}
}
复制代码
假设分割视图控制器没有它自己的状态,我们就可以把它的两个子视图控制器的状态合并起来作为分割视图控制器的状态。为此,可能我们最自然想到的就是这样:var state: (Master.State, Detail.State)
。但遗憾的是,元组类型没有实现 Codable,也无法通过条件化协议实现为它添加 Codable 支持 (实际上,元组无法实现任何协议)。因此,只能自己编写一个泛型结构体:Pair,然后给它添加 Codable 的条件化协议实现:
struct Pair<A, B>: Codable where A: Codable, B: Codable {
var left: A
var right: B
init(_ left: A, _ right: B) {
self.left = left
self.right = right
}
}
复制代码
最后,为了让 SplitViewController 实现 Restorable,我们必须要求 Master 和 Detail 也是实现了 Restorable 的类型。相对于在 SplitViewController 中单独保存一份组合的状态,可以直接从它的两个子视图控制器中计算出来。省去了这个局部变量,我们就把状态的修改立即传递到了两个子控制器:
extension SplitViewController: Restorable
where Master: Restorable, Detail: Restorable
{
var state: Pair<Master.State, Detail.State> {
get {
return Pair(master.state, detail.state)
}
set {
master.state = newValue.left
detail.state = newValue.right
}
}
}
复制代码
任何类型都只能实现协议一次。这就意味着我们不能再添加诸如 Master 实现了 Restorable,但是 Detail 没有 (或者反之),这样的协议实现条件了。
存在体
严格来说,在 Swift 中是不能把协议当作一个具体类型来使用的,它们只能用来约束泛型参数。 但下面的代码却可以通过编译 (使用了上面例子中的 DrawingContext 协议):
let context: DrawingContext = SVG()
复制代码
当我们把协议当作具体类型使用的时候,编译器会为协议创建一个包装类型,叫做存在体 (existential)。let context: DrawingContext
这种写法本质上就是类似
let context: Any<DrawingContext>
这种写法的语法糖。尽管这种语法并不存在,编译器会创 建一个 (32 字节的) Any 盒子,并在其中为类型实现的每个协议添加一个 8 字节的协议目击者。我们可以通过下面的代码来验证这个结果:
MemoryLayout<Any>.size // 32
MemoryLayout<DrawingContext>.size // 40
复制代码
为协议创建的这个盒子也叫做存在体容器 (existential container)。这是编译器必须要做的事情,因为它需要在编译期确认类型的大小。不同的类型自身大小有差异 (例如:所有的类都是一个指针的大小,而结构体和枚举的大小则依赖它们的实际内容),这些类型实现了一个协议的时候,把协议包装在存在体容器中可以让类型的尺寸保持固定,编译器也就能确定对象的内存布局了。
可以看到存在体容器的大小会随着类型实现协议的增多而增长。例如,Codable 是 Encodable 和 Decodable 的组合,所以,我们可以预期 Codable 存在体的大小是 32 字节的 Any 容器,加上 2 个 8 字节的协议目击者:
MemoryLayout<Codable>.size // 48
复制代码
当我们创建一个 Codable 数组的时候,无论数组中元素的具体类型是什么,编译器都可以确认,
每个元素的大小是 48 字节。例如,下面这个包含了三个元素的数组,将会占用 144 字节空间:
let codables: [Codable] = [Int(42), Double(42), "fourtytwo"]
复制代码
对于 codables 数组中的元素,唯一能做的事情,就是调用 Encodable 和 Decodable 中的 API (这是指不用 as,as? 或 is 等运行时类型转换的条件下)。因为元素的具体类型已经被存在体容器隐藏起来了。
有时,存在体和带有类型约束的泛型参数是可以交换使用的。来看下面这两个函数:
func encode1(x: Encodable) { }
func encode2<E: Encodable>(x: E) { }
复制代码
尽管这两个函数都可以用一个实现了 Encodable 的类型调用,但它们并不完全相同。对于 encode1 来说,编译器会把参数包装到 Encodable 的存在体容器里。这个包装不仅会带来一些性能开销,如果要包装的值过大以至于无法直接存放到存在体里,就还需要开辟额外的内存空间。可能更重要的是,这还会阻止编译器的进一步优化,因为对被包装类型的所有方法调用都只能经过存在体中的协议目击者表完成。
而对于泛型函数,编译器可以为部分或者所有传递给 encode2 的参数类型生成一个特化的版本。这些特化版本的性能,和我们手工去为这些类型重载 encode2 是完全一样的。而相比 encode1,泛型方式实现的缺点,则是更长的编译时间以及更大的二进制程序。
对大多数代码来说,存在体带来的性能开销不是问题,但当你编写一些性能关键的代码时,就要把这个影响考虑进来。如果你在一个循环里调用上千次上面这两个 encode 函数,就会发现 encode2 要快的多得多。
存在体和关联类型
在 Swift 5 里,存在体只针对那些没有关联类型和 Self 约束的协议
let collections: [Collection] = ["foo", [1]]
// 错误: 'Collection' 只能用做泛型参数约束
// 因为它包含了 Self 或关联类型约定。
复制代码
上面这段代码并不合理:不能在不指定关联类型 Element 的情况下使用 Collection。现在,这是个 Swift 中的硬性规定,但在未来的 Swift 版本里,可能会写出类似下面这样的代码:
// 并不是真正的 Swift 语法
let collections: [any Collection where .Element == Character] = ["foo", ["b"]]
复制代码
而对于那些包含 Self 约束的协议,这个限制是类似的。例如,考虑下面这段代码:
let cmp: Comparable = 15 // 编译错误
复制代码
定义在 Comparable 中的操作符 (以及从 Equatable 中继承来的操作符) 希望用于比较的两个参数的类型是完全一致的。如果允许定义 Comparable 类型的变量,你就可能会用 Comparable 中的 API 来比较它们,例如:
(15 as Comparable) < ("16" as Comparable)
// 错误:二进制操作符 '<' 不能用于两个 'Comparable' 操作数。
复制代码
但是,这样的写法完全不合理,因为直接比较字符串和整数是不可能的。因此,编译器禁止为包含关联类型约束的协议 (或者使用了 Self 的协议,本质上这也是一种关联类型) 生成存在体。
类型消除器
尽管无法为带有 Self 或关联类型约束的协议创建存在体,但可以编写一个执行类似功能的函数,叫做:类型消除器 (type erasers)。
let seq = [1, 2, 3].lazy.filter { $0 > 1 }.map { $0 * 2 }
复制代码
它的类型是 LazyMapSequence<LazyFilterSequence<[Int]>, Int>
。随着串联更多的操作,这个声明就会更加复杂。有时,会想要消除掉结果中类型的细节,只得到一个包含 Int 元素的序列就好了。虽然不能通过存在体表达这个想法,但可以用 AnySequence 隐藏掉原始的类型:
let anySeq = AnySequence(seq)
复制代码
anySeq 的类型就是 AnySequence<Int>
。尽管这看上去简单多了,并且用起来也和一个序列一样,但这样做也是有代价的:AnySequence引入了额外的一层间接性,它比直接使用被隐藏的原始类型慢一些。
标准库为很多协议都提供了类型消除器,例如:AnyCollection 和 AnyHashable。我们给之前定义的 Restorable 协议实现一个简单的类型消除器。
可能会写一个像下面这样的 AnyRestorable。但它并不能完成任务。因为泛型参数 R 直接就暴露了要隐藏的协议,这个版本的 AnyRestorable就跟形同虚设的一样:
struct AnyRestorable<R: Restorable> {
var restorable: R
}
复制代码
实际上,希望 AnyRestorable 的泛型参数反映的应该是 State (译注:就像 AnyCollection 的泛型参数是集合中元素的类型,而不是 Collection 协议一样)。为了让 AnyRestorable 实现 Restorable,我们还需要提供 state 属性。为此,使用和标准库同样的实现方法:它使用了三个类来实现一个类型消除器。首先,我们创建一个实现了 Restorable 的类:
AnyRestorableBoxBase。访问它的 state 属性,会直接导致 fatalError。因为这个类是实现细节的一部分,永远都不应该直接创建这个类型的对象:
class AnyRestorableBoxBase<State: Codable>: Restorable {
internal init() { }
public var state: State {
get { fatalError() }
set { fatalError() }
}
}
复制代码
其次,创建一个 AnyRestorableBoxBase 的派生类,让它带有一个实现了 Restorable 的泛型参数 R。这里,让类型消除器得以工作的伎俩,就是限制了 AnyRestorableBoxBase 的泛型参数和 R.State 是同一个类型:
class AnyRestorableBox<R: Restorable>: AnyRestorableBoxBase<R.State> {
var r:R
init(_ r: R) {
self.r = r
}
override var state: R.State {
get { return r.state }
set { r.state = newValue }
}
}
复制代码
这种派生关系意味着,我们可以创建一个 AnyRestorableBox 实例,但把它当成一个 AnyRestorableBoxBase 来用。由于 AnyRestorableBoxBase 实现了 Restorable,进而,它又 可以直接当成 Restorable 来用。最后,我们创建一个包装类,AnyRestorable,把 AnyRestorableBox 藏起来:
class AnyRestorable<State: Codable>: Restorable {
let box: AnyRestorableBoxBase<State>
init<R>(_ r: R) where R: Restorable, R.State == State {
self.box = AnyRestorableBox(r)
}
var state: State {
get { return box.state }
set { box.state = newValue }
}
}
复制代码
总体来说,当编写一个类型消除器的时候,要确保它包含了协议约束的所有方法。尽管编译器可以在这件事情上帮一把,但它会放过那些带有默认实现的协议方法。在类型消除器里,不能依赖这些默认实现,而是要始终把方法调用转发到被隐藏的原始类型上,因为它们都是有可能被定制的。
滞后于类型定义的协议实现
Swift 中协议的一个主要特性就是一个类型对协议的实现可以滞后于类型定义本身。例如,在这一章始,让 CGContext 实现了 Drawable。当让一个类型实现某个协议的时候,我们应该确保自己要么是这个类型的拥有者,要么是这个协议的拥有者 (当然也可以两者兼顾)。但是,让一个不属于你的类型实现一个不属于你的协议则是一个不被推荐的做法。
例如,Core Location 框架中的 CLLocationCoordinate2D 并没有实现 Codable 协议。尽管给它加上这个支持很容易,但如果 Apple 决定给 CLLocationCoordinate2D 加入官方 Codable 支持,自己的实现可能就无法编译了。在这种情况下,Apple 可能会选择一种不同的实现方式,结果就是,我们可能无法反序列化已经存在的文件格式了。
当不同的程序包里存在着同一个类型对同一个协议实现的时候,也会发生实现冲突。这曾经在 SourceKit-LSP 和 SwiftPM 中发生过。因为它们都给 Range 实现了 Codable,只是协议约 的条件不同。结果在 Swift 5 里,标准库中为 Range 实现了 Codable。
作为这些潜在问题的一个解决方案,可以创建一个包装类型,并给它添加条件化协议实现。 例如:可以创建一个包含 CLLocationCoordinate2D 的结构体,并让这个结构体实现 Codable。