木又的《Swift进阶》读书笔记——枚举

751 阅读8分钟

枚举

  • 结构体和类都是记录类型 (record type)。一个记录由零个或多个具有类型的字段 (属性) 组成。元组也属于记录类型:实际上它是一个功能较少的轻量级的匿名结构体。Swift 的枚举属于一个完全不同的类别,有时称它为 标签联合变体类型 (variant type)

概述

  • 一个枚举由零个或多个 成员(case) 组成,每个成员都可以有一个元组样式的关联值 (associated value) 列表。

枚举是值类型

就像结构体一样,枚举也是值类型。它的能力几乎和结构体相同:

  • 枚举可以有方法,计算属性和下标操作。
  • 方法可以被声明为可变和不可变。
  • 你可以为枚举实现扩展。
  • 枚举可以实现各种协议。

但枚举不能拥有存储属性。一个枚举的状态完全由它的成员和成员的关联值组合起来表示。对于某个特定的成员,可以将关联值视为其存储属性。

总和类型和乘积类型

  • 一个枚举值只会包含一个枚举成员 (如果这个成员有关联值的话,则再加上关联值)。
  • 通常来说,一个元组 (或者结构体,类) 的居民数量,等于其成员的居民数量的乘积。因此,结构体,类和元组也被成为乘积类型 (Product Types)
  • 一般而言,一个枚举的居民数量,等于他所有成员的居民数量的总和。因此称枚举为总和 (Sum Types) 的原因。

模式匹配

  • 模式匹配不是 switch 所独有的,但却是最明显的用例。

  • Swift 支持的模式类型:

    通配符模式 — 符号为下划线:_。它匹配任意值并忽略这个值。

    元组模式 — 使用一个用逗号分割的子模式 (subpattern) 列表来匹配元组。

    枚举成员模式 — 匹配指定的枚举成员。它可以包含子模式来处理关联值,像是等式检查 (.success(42)) 或值绑定 (.failure(let error)) 这种。

    值绑定模式 — 把一个匹配值的部分或全部,绑定到一个新的常量或变量上。

    可选值模式 — 通过使用我们所熟悉的问号语法,为匹配及解包可选值这两个操作,提供了一个语法糖。

    类型转换模式 — 模式 is SomeType 匹配成功的条件是,一个值的运行时类型必须是 SomeType 或是其子类。

    表达式模式 — 通过把输入值和模式作为参数传递给定义在标准库中的模式匹配操作符 (~=) 来匹配表达式。

在其他上下文中的模式匹配

  • 虽然模式匹配是从枚举中提取关联值的唯一方式,但它不是专属于枚举或 switch 语句的。

使用枚举进行设计

Swift 语句的完备性

  • 一个 switch 语句必须是完备的。
  • 完备性检查的最大好处体现在如果你想让枚举和使用它的代码是同步演进的时候。

不可能产生非法的状态

为什么要使用像是 Swift 这种静态类型语言。性能是其中之一:编译器对于程序中变量的类型知道的越多,通长就越能产生更快的代码。

另一个同样重要的理由是,类型系统可以指导开发人员应该如何使用API。

使用枚举来实现状态

  • 一个系统可以存在的状态集合也被称为其状态空间。
  • 尝试使你的状态空间尽可能的小。
  • 枚举不是完整的有限状态机 (finite-state machines), 因为它缺乏指定非法状态转换的能力。

在枚举和结构体之间做选择

  • 如果我们让结构体的初始化方法的访问级别为 internal 或 public 的话,则可以在其他文件或者上甚至其他模块中通过添加静态方法或属性来扩展这个结构体,从而添加新的分析事件到API中。枚举的版本是无法是实现这一点的:你不能在其他地方添加新的成员到枚举中。
  • 枚举可以更精确地实现数据类型;它只能表示预定义成员中的一个,但结构体因为这两个属性而可能表示无限多的值。如果你想对事件做进一步的处理 (例如,合并事件序列),则枚举的精确性和安全性会派上用场。
  • 结构体可以有私有“成员” (也就是说,对所有使用者都不可见的静态方法或静态属性),而枚举中成员的可见性始终和枚举本身保持一致。
  • 你可以对枚举使用 switch 语句,并利用语句的完备性来确保不会错过任何一个事件的类型。但由于这种严格性,所以向枚举添加一个新的事件类型就可能会破坏使用这个 API 用户的源代码,但你可以为新的十几件类型往结构体中添加静态方法,而不用担心会影响其他代码。

枚举和协议之间的相似之处

  • 枚举协议 都可以表示“之一”关系结构。
  • 以一个包含 形状类型渲染方法Shape 为例,枚举和协议在组织代码上有差别。基于枚举的实现是按方法来分组的:所有形状类型的 CGContext 渲染代码都在 render(into:) 方法中的单个 switch 语句中。另一方面,基于协议的实现是按“成员”来分组的:每个具体的类型都实现自己的 render(into:) 方法,该方法中包含了每个形状特定的渲染代码。这导致了在扩展维度上的差异,枚举的实现便于渲染方法的添加,协议的实现便于形状的添加。

使用枚举实现递归数据结构

  • 枚举非常适合用来实现递归数据结构,即“包含”自身的数据结构。

    /// 一个单向链表
    enum List<Element> {
      case end
      indirect case node(Element, next: List<Element>)
    }
    

    注意 indirect 关键字,这使代码能编译通过所必需的。indirect 告诉编译器把 node 成员表示为一个引用,从而使递归起作用。indirect 语法仅适用于枚举。

原始值 (Raw Value)

给枚举指定一个原始值需要在枚举名字后面加上原始值的类型并用冒号分隔开。然后使用复制语法给每个成员赋值一个原始值。

enum HTTPStatus: Int {
  case ok = 200
  case created = 201
  case movedPermanently = 301
  case notFound = 404
}

每个成员的原始值必须唯一。

RawRepresentable 协议

一个实现 RawRepresentable 的协议的类型会获得两个新的 API:一个rawValue 属性和一个可失败的初始化方法 (init?(rawValue:))。

/// 一个可以同相关原始值做转换的类型
protocol RawRepresentable {
  /// 原始值的类型,例如 Int 或 String。
  associatedtype RawValue
  
  init?(rawValue: RawValue)
  var rawValue: RawValue { get }
}

手动实现 RawRepresentable

enum AnchorPoint {
  case center
  case topLeft
  case topRight
  case bottomLeft
  case bottomRight
}

extension AnchorPoint: RawRepresentable {
  typealias RawValue = (x: Int, y: Int)
  
  var rawValue: (x: Int, y: Int) {
    switch self {
      case .center: return (0,0)
      case .topLeft: return (-1,1)
      case .topRight: return (1,1)
      case .bottomLeft: return (-1,-1)
      case .bottomRight: return (1,-1)
    }
  }
  
  init?(rawValue: (x: Int,y: Int)) {
    switch rawValue {
      case (0,0): self = .center
      case (-1,1): self = .topLeft
      case (1,1): self = .topRight
      case (-1,-1): self = .bottomLeft
      case (1,-1): self = .bottomRight
      default: return nil
    }
  }
}

让结构体和类来实现 RawRepresentable

struct UserID: RawRepresentable {
  var rawValue: String
}

原始值的内部表示

一个枚举类型的实例所能拥有值只能是其成员的一个。获取原始值的唯一方式就是通过调用 rawValue 和 init?(rawValue:) 这两个 API。

列举枚举值

把一个类的居民作为一个集合进行操作的这个需求通常是很有用户的,例如,迭代或计数它们。Caselterable 协议通过添加一个静态属性 allCases 来实现这个功能。

/// 一个提供其所有值集合的类型
protocol Caselterable {
  associatedType AllCases: Collection
  	where AllCases.Element == Self
  
  static var allClases: AllCases { get }
}

手动实现 Caselterable

extension Bool: Caselterable {
  public static var allCases: [Bool] {
    return [false, true]
  }
}

Bool.allCases // [false, true]

固定和非固定枚举

  • 可能会在未来添加新成员的枚举,称之为非固定。
  • @unknown default 为你提供了两全其美的方案:编译时的完备性检查和运行时的安全性。
  • @frozen 用于将一个特定的枚举声明为固定的。通过使用此属性,库的开发者就等于做出一个永远不会向被标记的枚举中添加新的成员的承诺 - 要不然如果这样做的话,就会破坏二进制的兼容性。
  • 在标准库中,固定枚举的例子包括有 OptionalResult;如果它们不是固定的话,switch 它们时就总是会需要一个 default 子句,这是一个很大的烦恼。

提示和窍门

  • 尽量避免使用嵌套 switch 语句。
  • 利用明确初始化检查 (definite initialization check)。
  • 避免用 nonesome 来命名成员。
  • 对那些用保留的关键字来命名的成员使用反引号 (backtick)。
  • 可以像工厂方法一样使用成员。
  • 不要使用关联值来模拟存储属性。请改用结构体。
  • 不要过度使用关联值组件。
  • 把空枚举作为命名空间。