木又的《Swift进阶》读书笔记——协议

协议

当我们使用泛型类型的时候,通常都会使用协议约束泛型参数的行为。有很多理由使得你应该如此,下面就是一些最常见的例子:

  • 通过协议,你可以构建一个依赖数字 (而不是诸如 Int,Double 等某个具体的数值类型) 或集合类型的算法。这样一来,所有实现了这个协议的类型就都具备了这个心算法提供的能力。
  • 通过协议还可以抽象代码接口背后的实现细节,你可以针对协议进行编程,然后让不同的类型实现这个协议。例如,一个使用了 Drawable 协议的画图程序既可以使用 SVG 来渲染图形,也可以使用 Core Graphics。类似的,跨平台的代码可以使用一个 Platform 协议,然后由类似 Linux,macOS 或 iOS 这样的类型来提供具体的实现。
  • 你还可以使用协议来让代码更具可测试性。更具体地说,当你基于协议而不是一个具体类型来实现某个功能的时候,在测试用例中就很容易把这部分替换成表示各种测试结果的类型。

在 Swift 里,一个协议表示一组正式提出的 要求 (requirements)。例如,Equatable 协议要求实现的类型提供 == 操作符。这些要求可以是普通方法、初始化方法、关联类型、属性和继承的协议。有些协议还有一些无法用 Swift 类型系统表达的要求,例如,Collection 协议就要求下标操作符访问元素的时间复杂度是 0(1) (但你也可以违背这个要求,如果算法的时间复杂度不是O(1),在方法的文档中明确说明就行了。)

Swift 协议的主要特性。

  • 协议可以自行扩展新的功能。
  • 协议可以通过条件化扩展(conditional extensions)添加需要额外约束的 API。
  • 协议可以继承其他协议。
  • 协议可以被组合起来形成新的协议。
  • 有时,某个协议的实现还依赖于其他协议的实现。
  • 协议还可以声明关联类型,实现了这个协议的类型就需要定义关联类型对应的具体类型。

协议目击者

struct Eq<A> {
  let eq:(A,A) -> Bool
}
复制代码

现在,我们就可以为比较不同的具体类型 (例如: Int) 创建不同的 Eq 实例了。我们管这些实例,叫做表示相等判断的 显示目击者 (explicit witnesses)

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
  }
}
复制代码

下面是通过协议取代了显示目击者 (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
  }
}
复制代码

相比泛型参数A,我们可以使用隐式泛型参数 Self,用它表示实现了协议的类型:

extension Equatable {
  static func notEqual(_ l:Self, _ r:Self) -> Bool {
    return !(l == r)
  }
}
复制代码

条件化协议实现 (Conditional Conformance)

标准库中 Array 对 Equatable 的实现:

extension Array: Equable where Element: Equatable {
  static func == (lhs:[Element], rhs:[Element]) -> Bool {
    fatalError("Implementation left out")
  }
}
复制代码

协议继承

Swift 还支持协议的继承。例如,实现 Comparable 的类型也一定实现了 Equatable。这叫做 细化 (refinging)。换句话说,Comparable 改进了 Equatable:

public protocol Comparable: Equatable {  static func < (lhs: Self, rhs: Self) -> Bool  // ...}
复制代码

使用协议进行设计

举个绘图协议的例子,首先,定义一个要求实现绘制椭圆和矩形接口的协议:

protocol DrawingContext {
  mutating func addEllipse(rect: CGRect, fill: UIColor)
  mutating func addRectangle(rect: CGRect,fill:UIColor)
}
复制代码
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)
  }
}
复制代码
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)。只要知道了如何绘制椭圆,就可以添加一个扩展来以某点为圆心绘制圆形。

extension DrawingContexnt {  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 drawingSomething() {    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 标准库的做法: 只要你实现了协议要求的几个少数方法,就可以“免费”收获这个协议通过扩展得到所有功能。

定制协议扩展

通过扩展给协议添加的方法,并不作为协议约束的一部分。在某些情况下,这会导致出乎意料的结果。

只有协议目击者中的方法才能被动态派发到一个具体类型对应的实现,因为只有目击者中的信息在运行时是可用的。在泛型上下文环境中,调用协议中的非约束方法总是会被静态派发到协议扩展中的实现。

为了获得动态派发的行为,我们应该让 addCircle 成为协议约束的一部分。这样,在协议扩展中 addCircle 的实现就变成了协议约束的 默认实现。带有默认实现的协议方法在 Swift 社区中有时也叫做 定制点 (customization point)。实现协议的类型会收到一份方法的默认实现,并有权决定是否要对其进行覆盖。

协议组合

协议可以被组合在一起。

typealias Codable = Decodable & Encodable
复制代码

协议继承

协议之间还可以是继承关系。实际上,typealias Codable = Encodable & Decodable 这种写法在语法上,和 protocol Codable: Encodable & Decodable 是完全一样的。只是别名的写法看上去稍微简洁了一点,它更明确地告诉我们:Codable 仅仅 是这两个协议的组合,并没有在组合的结果里添加任何新的方法。

协议和关联类型

有些协议需要约束的不仅仅是方法、属性和初始化方法,它们还希望和它相关的一些类型满足特定的条件。这就可以通过关联类型 (associated type) 来实现。

举个栗子,通过协议关联类型,重新实现一个小型的 UIKit 状态恢复机制。

在 UIKit 里,状态恢复需要读取视图控制器以及视图的架构,并在 app 挂起的时候将它们的状态序列化。当 App 下一次加载的时候,UIKit 会尝试恢复应用程序的状态。

protocol ViewController {}
复制代码
protocol Restorable {
  associatedtype State: Codable
  var state: State { get set }
}
复制代码

创建一个显示消息的视图控制器。这个视图控制器的状态由一个消息数组以及当前的滚定位置构成,我们把它定义成一个实现了 Codable 的内嵌类型:

class MessagesVC: ViewController, Restorable {  typealias State = MessagesState  struct MessagesState: Codable {    var messgaes: [String] = []    var scrollPosition: CGFloat = 0  }  var state: MessagesState = MessgaesState()}
复制代码

基于关联类型的条件化协议实现

有些类型只有在特定条件下才会实现一个协议。

extension Range: Sequence
	where Bound: Strideable, Bound.Stride: SignedInteger
复制代码

存在体

严格来说,在 Swift 中是不能把协议当作一个具体类型来使用的,它们只能用来约束泛型参数。但让人诧异的是,下面的代码却可以通过编译:

let Context:DrawingContext = SVG()
复制代码

当我们把协议当作具体类型使用的时候,编译器会为协议创建一个包装类型,叫做 存在体 (existential)let context:DrawingCon 这种写法本质上就是类似 let context: Any<DrawingContext> 这种写法的语法糖。尽管这种语法并不存在,编译器会创建一个 (32字节的) Any 盒子,并在其中为类型实现的每个协议添加一个 8 字节的协议目击者。

MemoryLayout<Any>.size // 32
MemoryLayout<DrawingContext>.size // 40
复制代码

为协议创建的这个盒子也叫做 存在体容器 (existential container)。这是编译器必须要做的事情,因为它需要在编译期确认类型的大小。不同的类型自身大小有差异 (例如:所有的类都是一个指针的大小,而结构体和枚举的大小则依赖他们的实际内容),这些类型实现了一个协议的时候,把协议包装在存在体容器中可以让类型的尺寸保持固定,编译器也就能确定对象的内存布局了。

MemoryLayout<Codable>.size // 48
let codables: [Codable] = [Int(42), Double(42), "fourtytwo"] // 占用144(48 * 3)字节空间
复制代码

存在体和关联类型

在 Swift 5 里,存在体只针对那些没有关联类型和 Self 约束的协议。

let collections: [Collection] = ["foo", [1]]
// 错误: 'Collection' 只能用作泛型参数约束
// 因为它包含了 Self 或关联类型约定。
复制代码

我们不能在不指定关联类型 Element 的情况下使用 Collection。

类型消除器

尽管我们无法为带有 Self 或关联类型约束的协议创建存在体,但我们可以编写一个执行类似功能的函数,叫做:类型消除器 (type erasers)

let seq = [1,2,3].lazy.filter { $0 > 1 }.map { $0 * 2 }
复制代码

seq 的类型是 LazyMapSequence<LazyFilterSequence<[Int]>, Int>

我们会想要消除掉结果中类型的细节,只得到一个包含 Int 元素的序列就好了。可以用 AnySequence 隐藏掉原始的类型:

let anySeq = AnySequence(seq)
复制代码

anySeq 的类型就是 AnySequence<Int>。尽管这看上去简单多了,并且用起来也和一个序列一样,但这样做也是有代价的:AnySequence 引入了额外的一层间接性,它比直接使用被隐藏的原始类型慢一些。

标准库为很多协议都提供了类型消除器,例如:AnyCollectionAnyHashable

下面我们给之前定义的 Restorable 协议实现一个简单的类型消除器。

class AnyRestorableBoxBase<State: Codable>: Restorable {
  internal init() {}
  public var state: State {
    get { fatalError() }
    set { fatalError() }
  }
}
复制代码
class AnyRestorableBox<R: Restorable>: AnyRestorableBoxBase<R.State> {
  var r: R
  int(_ r: R) {
    self.r = r
  }
  
  override var state: R.State {
    get { return r.state }
    set { r.state = newValue }
  }
}
复制代码
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 中协议的一个主要特性就是一个类型对协议的实现可以之后滞后于类型定义本身。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享