Structs and Classes, Enums

Structs and Classes

  • Both can have stored and computed properties
  • and both can have methods defined on them
  • Furthermore, both have initializers
  • we can define extensions on them,
  • and we can conform them to protocols.
  • Sometimes our code even keeps compiling when we change the class keyword into struct or vice versa.

Value Types and Reference Types

value types: assignment copies the value.

public struct Int: FixedWidthInteger, SignedInteger {
    // ...
}

variable: a name for a location in memory that contains a value of a certain type.

The statement b += 1 takes the value stored in the memory location referred to as b, increments it by one, and writes it back to the same location in memory.

assigning a struct to another variable creates an independent copy of the value.

reference types:

  • variables don’t contain the “thing” itself, but a reference to it.
  • Other variables can also contain a reference to the same underlying instance, and the instance can be mutated through any of its referencing variables.

Mutation

If we store the instances in let variables, we can still mutate the class instance, but not the struct instance.

因为let意味着变量指向的东西是不可变的, 而值变量指向的是值, 当然不能变, 引用变量指向的却是地址, 址址不能变, 但是地址里存的东西却是可变的(或者说, 我只是不能让它指向新的地址而已).

  • 相对的, 值变量包含了整个值(即使是个复杂的结构体, 也是整个值).
  • 哪怕只是修改结构体里用var声明的变量, 也意味着给整个结构体赋了一个全新的值:
scoreStruct.guest += 1
// is equivalent to:
scoreStruct = ScoreStruct(home: scoreStruct.home,
guest: scoreStruct.guest + 1)

层级属性也一样. 比如rect.origin.x = 10.

如果反过来, 对指向实例的变量声明为var, 但每一个属性都声明为let呢? 这样你将不能修改实例的属性, 但是可以把这个变量指向新的实例.

Mutating Methods

  • Normal methods on structs defined with the func keyword can’t mutate any of the struct’s properties.
  • We can only call mutating methods on variables declared with var

inout Parameters

参数默认都是let的:

func scoreGuest(_ score: ScoreStruct) {
    score.guest += 1
    // Error: Left side of mutating operator isn't mutable:
    // 'score' is a 'let' constant.
}

使用inout:

// 1. 加上inout
func scoreGuest(_ score: inout ScoreStruct) {
    score.guest += 1
}
var scoreStruct3 = ScoreStruct(home: 0, guest: 0)
scoreGuest(&scoreStruct) // 2, 使用&
scoreStruct3.guest // 0

前面的章节已经说过一次, 这里面的&符号并不是代表"地址"或"引用", 仅仅是一个语法要求, 真实发生的是inout&一起使用时, score的值被复制了一份, 然后这个复制品被复制回原参数(所以原参数必须是var声明的), 函数修改的是这个复制品, 而不是原来的值.

  • 即使你哪怕没有去改变这个参数, 这个复制过程仍然存在(i.e. any willSet and didSet observers will fire)

Lifecycle

  • Structs are much simpler in this regard, because they can’t have multiple owners;
  • When the variable (contains the Struct) goes out of scope, its memory is freed and the struct disappears.
  • Swift uses Automatic Reference Counting (ARC) to keep track of the number of references to a particular instance.
  • When the reference count goes down to zero, the Swift runtime calls the object’s deinit and frees the memory.

Reference Cycles

两个或以上互相引用, 造成谁都无法deallocated, 会造成内存泄露, 且无法执行任何deinit方法(很多cleanup是会写在deinit里的). 引用循环可以有多种形式—从两个对象相互强引用,到由许多对象和闭包组成的复杂循环,这些闭包会捕获对象。

there are no references to structs

class Window {
    var rootView: View?
}
class View {
    var window: Window
        init(window: Window) {
        self.window = window
    }
}
var window: Window? = Window() // window: 1
var view: View? = View(window: window!) // window: 2, view: 1
window?.rootView = view // window: 2, view: 2
view = nil // window: 2, view: 1
window = nil // window: 1, view: 1

这里就多了两段内存, 它们内部各有一个指针, 指向了对方(这也是他们没有被释放的原因), 但是整个系统里却没有其他地方引用它们, 所以它们无法被释放, 造成了内存泄露.

Weak References

Weak references in Swift are always zeroing: the variable will automatically be set to nil once the referred object gets deallocated — this is why weak references must always be optionals.

class Window {
    weak var rootView: View?
    deinit {
        print("Deinit Window")
    }
}
class View {
    var window: Window
    init(window: Window) {
        self.window = window
    }
    deinit {
        print("Deinit View")
    }
}
var window: Window? = Window() // window: 1
var view: View? = View(window: window!) // window: 2, view: 1
window?.rootView = view // window: 2, view: 1(这是关键, weak不增加计数)
window = nil // 这时个还有个view在强引用它, deinit没触发
view = nil // view的引用没了, 因为windows的弱引用不计数, 顺利进入deinit, 顺便windows的强引用也释放了, windows的deinit也顺利进入:
/* 依次打印:
Deinit View
Deinit Window
*/

要了解这两个例子, 最重要的是要了解内存本身的deinint才会引起引用计数减1, 而不是指向它的变量被设置为nil, 设nil这个行为在语义上其实只代表引用计数减1. 对于上面的例子, 代表View的那一块内存, 被一个view变量强引用, 还被一个weak引用, 但是不引起计数变理, 所以当view = nil的时候, 这块内存的引用计数立刻就变成了0, 顺利进入deinit, 然后内存被释放, Window的强引用释放是发生在View真实内存的释放.

Unowned References

Sometimes, though, we want a non-strong reference that is not optional.

改造上面的例子, 让Window强引用View, 但Viewunowned引用Window, 会造成View内存块有两个引用, 但是Window内存块只有一个引用, 所以view变量释放的时候, 什么也不会发生, 当windows变量设nil的时候, 唯一引用消失, 顺利deinit, 内存释放, 使得View的引用也减了1, 变成了0, 也顺利释放.

但是

如果先释放的Window, 因为它的引用只有1, 所以会立即释放, 但这个时候view对象还unowned引用着它, 一旦真的去访问这个window, 程序就会崩溃. 所以不管是weak, 还是unowned, 其目的只是不让引用计数加1, 其它的后果需要自己小心处理.

The Swift runtime keeps a second reference count in the object to keep track of unowned references.

Closures and Reference Cycles

If a closure captures a variable holding a reference type, the closure will maintain a strong reference to it.

object A references object B, but object B stores a closure that references object A

class Window {
    var onRotate: (() -> ())? = nil
}

var window: Window? = Window()
var view: View? = View(window: window!)
window?.onRotate = {
    print("We now also need to update the view: \(view)")
}
  1. View内存里有Window的引用
  2. Window的内存里有onRotate闭包的引用
  3. onRotate闭包里引用了View的内存地址(通过view变量)

所以, 三个内存块都互相引用, 导致内存泄漏. 所以有三个地方可以尝试打破这个引用循环:

  1. View弱引用Window: 因为Window没有别的引用, 这将使得Window立即释放
  2. 把闭包标识为weak: Swift不支持
  3. 闭包里弱引用view: BINGO!
window?.onRotate = { [weak view, weak myWindow=window, x=5*5] in
    print("We now also need to update the view: \(view)")
    print("Because the window \(myWindow) changed")
}

方括号里的叫capture list, 它不但可以用来定义weakunowned引用, 甚至可以定义毫不相关的变量.

Choosing between Unowned and Weak References

如果你知道一个对象在你需要使用的时候一定存在, 所以只是在语法上要打破引用循环, 那么使用unowned引用会更合适, 因为它更方便, 是non-optional的(崩溃风险自负). 否则, 使用weak引用. 实践中, 使用weak更多, 因为写代码时算好的场景很容易在将来的变更中被打破从而引起崩溃.

就像你知道一个Optional变量在你使用的时候一定存在, 那么你可以使用!来解包, 写起来也是最简单, 但是如果不确定, 那么则需要使用if let或者guard let代码块来解包.

Deciding between Structs and Classes

To share ownership of a particular instance (and represent the same value), we have to use a class. Otherwise, we can use a struct. 基本上, 你一个值可以随便赋给其它变量, 也不关心它的后续变动, 就可以选择结构体了 比如URL. 如同int, string, bool, 这一类值我们并不关心一样的值后面是不是同一块内存.

相反, 如果两个UIView, 哪怕每个属性一样, 它们也是在屏幕上的两个独立的实体, 以及view hierarchy中的两个节点, 所以它们需要是class, 能够以引用的方式在程序中传递.(它可能是superviewsubView, 也可以是child的superview, 也可以纯粹被ViewController所引用)

Structs are a tool in our toolbox that are purposefully less capable than classes. In return, structs offer simplicity: no references, no lifecycle, no subtypes.

In addition, structs promise better performance, especially for small values.

但是, 某些场景使用值语义(Value Semantics)的class, 可以在扩展性和简单性上取得更好的权衡.

Classes with Value Semantics

  • First we declare all properties as let, making them immutable.
  • Then we make the class final to disallow subclassing
final class ScoreClass {
    let home: Int
    let guest: Int
    init(home: Int, guest: Int) {
        self.home = home
        self.guest = guest
    }
}
let score1 = ScoreClass(home: 0, guest: 0)
let score2 = score1

因为一切都是let的, 所以可以把它当成值类型使用, 但是它并没有在赋值时真的发生复制.

NSArray itself doesn’t expose any mutating APIs, so its instances can essentially be used as if they were values. 事实要更复杂, 因为它有一个可变版本的NSMutableArray, 所以造成好的编程实践是对任何不可靠来源的NSArray都使用copy方法先复制一份.

Structs with Reference Semantics

结构体的属性可以是一个class类型, 这样的话, 在发生复制时, 这个引用类型的属性也会被复制, 这样结构体就具备了部分引用类型的特性.

Copy-On-Write Optimization

Value types require a lot of copying, the standard library’s collection types (Array, Dictionary, Set, and String). They are all implemented using copy-on-write.

  • Copy-on-write means that the data in a struct is initially shared among multiple variables;
  • the copying of the data is deferred until an instance mutates its data.
  • Copy-on-write behavior is not something we get for free for our own types; we have to implement it ourselves

实现自己的copy-on-write还是看教程吧, 这里不展开了.

Enums

  • Structs and classes, which we discussed in the previous chapter, are examples of record types.
  • A record is composed of zero or more fields (properties), with each field having its own type.
  • Tuples also fall into this category: a tuple is effectively a lightweight anonymous struct with fewer capabilities.

Swift’s enumerations, or enums, belong to a fundamentally different category that’s sometimes called tagged unions or variant types.

A case can have multiple associated values, but you can think of them as a single tuple.

Enums Are Value Types

  • Enums can have methods, computed properties, and subscripts.
  • Methods can be declared mutating or non-mutating.
    * we mutate an enum by assigning a **new value**
    
    directly to self.
  • You can write extensions for enums.
  • Enums can conform to protocols.
  • Enums cannot have stored properties
    • 有assocated value就够了, 相当于特定case下的stored properties

Sum Types and Product Types

一个具体的枚举只能代表其中一个case, 而一个Struct, 里面每个属性可以有自己的值, 就其能表达的状态而言(以tuple为例):

  • 一个枚举如果有两个case, 那他就只能表达两个状态
  • 如果有第三个case, 这个case有一个bool类型的关联值, 那么它就能表达4个状态
  • 所以它的状态数是累加的: 1+1+2=4, 这种叫Sum Type
  • 如果一个元组有两个bool值, 那么这个元组能表达4个状态: (true, true); (true, false); (false, true); (false, false)
  • 如果有三个bool值, 立刻就能表达8个状态, 因为增加了2个状态:2*4=8, 这种叫Product Type

有什么用?

后面的章节我们知道了, 你所有的类型都是为了表现app的状态, 所以你设计的变量如果状态越多, 你就需要在代码里穷尽所有的状态, 才能编写出安全的代码. 事实上苹果自己的API在这一点上做得也不尽然, 有很多Product Type的出差, 能组合出事实上不存在的状态, 要么你选择在逻辑上有问题的代码, 去忽视掉这些状态, 要么你只能对每一种可能性都进行判断. 至少编译器是不知道你的意图和事实情况的, 所以他也就没法给你做出正确的警告或报错.

Pattern Matching

之前在Optional里, if-let binding, optional chaining, 及 Optional.map等就是match和unwrapping的例子. Pattern Matching在Swift中是增强版switch-case.

let result: Result<(Int, String), Error> = ...
switch result {
    case .success(42, _):
        print("Found the magic number!")
    case .success(_, let name):
        print("fond name: \(name)")
    case .success(_):
        print("Found another number")
    case .failure(let error):
        print("Error: \(error)")
}

借用的仍然是switch关键字, 但是能对任意associated value进行匹配, 而不仅仅是case的值. 上面演示了Wildcard pattern, Tuple pattern, value binding pattern, enum case pattern 等.

  • (let x, let y)也可以简化为let (x, y)
  • 但是(let x, y)则是捕获了第一个值, 而第二个值是精确地与y的值做对比
  • 支持where子句: .success(let httpStatus) where 200..<300 ~= httpStatus

Type-casting pattern

let input: Any = ...
switch input {
    case let integer as Int: ... // integer has type Int.
    case let string as String: ... // string has type String.
    default: fatalError("Unexpected runtime type: \(type(of: input)")
}

Expression pattern

标准库使用~=操作符来比较两个值是否相等, 但是也可以自定义这个操作符. 比如

  • Range类型, 200..<300 ~= httpStatus就是比较httpStatus是否在200..<300这个范围内.
  • Optional类型, ~=操作符就是比较两个Optional是否相等. Expression pattern允许你使用任意表达式来匹配值.
let randomNumber = Int8.random(in: .min...(.max))
switch randomNumber {
    case ..<0: print("\(randomNumber) is negative")
    case 0: print("\(randomNumber) is zero")
    case 1...: print("\(randomNumber) is positive")
    default: fatalError("Can never happen")
}

每一个case都是一个Expression pattern, 其实对应的就是~=操作符, 等同于:

if ..<0 ~= randomNumber { print("\(randomNumber) is negative") }
else if 0 ~= randomNumber { print("\(randomNumber) is zero") }
else if 1... ~= randomNumber { print("\(randomNumber) is positive") }
else { fatalError("Can never happen") }

如果是你自定义的类型, 你就得自定义~=操作符: func ~=(pattern: ???, value: ???) -> Bool

注意编译器会强制你default, 因为switch必须穷尽所有情况.

Pattern Matching in Other Contexts

模式匹配是提取枚举关联值的唯一方法。但是模式匹配并不局限于枚举,也不局限于 switch 语句。实际上,一个简单的赋值操作,例如let × =1可以被看作是在赋值运算符左侧的值绑定模式与右侧的表达式相匹配。其它场合:

Destructuring tuples in assignments

  • let (word, pi) = ("Hello", 3.1415)
  • for (key, value) in dictionary { ... }
  • for n in 1...10 where n.isMultiple(of: 3) { ... }
  • for (num, (key: k, value: v)) in dictionary.enumerated() { ... }

Using wildcards to ignore values

  • for _ in 1...3
  • _ = someFunction()

Catching errors in a catch clause

  • do { ... } catch let error as NetworkError { ... }.

if case and guard case statements

这一点在前面碰到过Collection, Optitonal

let color: ExtendedColor = ...
if case .gray = color {
    print("Some shade of gray")
}
if case .gray(let brightness) = color {
print("Gray with brightness \(brightness)")
}

for case and while case loops

Optional一节里看具体例子.

基本上, 模式匹配, 就是观察两边的模式和值, 看是否能把值绑定到模式对应的位置上.

Designing with Enums

Because enums belong to a different category of types than structs and classes do, they lend themselves to different design patterns.

1. Switch exhaustively

we recommend you avoid default cases in switch statements if at all possible. (不然编译器就没法警告了) 这样可以保证你每次添加新的case, 都必须同步更新switch语句. 同理, 如果你switch的是不受自己控制的来源, 你最好就要加上default case, 防止将来这个来源添加新的case, 导致你的代码崩溃.

let shape: Shape = ...
switch shape {
    case let .line(from, to) where from.y == to.y:
    print("Horizontal line")
    case let .line(from, to) where from.x == to.x:
    print("Vertical line")
    case .line(_, _):
    print("Oblique line")
    case .rectangle, .circle:
    print("Rectangle or circle")
}

2. Make illegal states impossible

一个不好的例子:

class CLGeocoder {
    func geocodeAddressString(_ addressString: String, 
        completionHandler: @escaping ([CLPlacemark]?, Error?) -> Void)
    // ...
}

回调函数里两个参数事实上不可能同时为空, 但设计上支持了这种可能性, 导致调用者必须处理这种情况. 如果改一改:

extension CLGeocoder {
    func geocodeAddressString(_ addressString: String, 
        completionHandler: @escaping (Result<[CLPlacemark], Error>) -> Void) {
    // ...
    }
}

结果和错误就由共存变成了二选一. 而且必有一. 这要就makes the invalid states unrepresentable

Many of Apple’s iOS APIs don’t take full advantage of Swift’s type system because they are written in Objective-C, which has no equivalent concept to enums with associated values. But that doesn’t mean we can’t do better in Swift.

3. Use enums for modeling state

A program’s state is the contents of:

  • all variables at a given point in time
  • plus (implicitly) its current execution state
    • i.e. which threads are running and which instruction they are executing.

4. Choosing between enums and structs

  • an enum value represents exactly one of its cases (plus its associated values),
  • whereas a struct value represents the values of all its properties.

5. Parallels between enums and protocols

上面说的是value, 即一个确定的枚举, 只代表一种场景, 一个确定的struct, 代表了它所有的属性的组合, 但是如果是这样建模的:

enum Shape {
    case line(from: Point, to: Point)
    case rectangle(origin: Point, width: Double, height: Double)
    case circle(center: Point, radius: Double)
}
extension Shape {
    func render(into context: CGContext) {
        switch self {
            case let .line(from, to): // ...
            case let .rectangle(origin, width, height): // ...
            case let .circle(center, radius): // ...
        }
    }
}

我们用枚举来抽象一个Shape定义, 用扩展来增加方法, 比如render, 相应的, 用结构体来建模会是这样:

protocol Shape {
    func render(into context: CGContext)
}
struct Line: Shape {
    var from: Point
    var to: Point
    func render(into context: CGContext) { /* ... */ }
}
struct Rectangle: Shape {
    var origin: Point
    var width: Double
    var height: Double
    func render(into context: CGContext) { /* ... */ }
}
// `Circle` type omitted.

则需要各建各的类, 通过实现同一个协议来共享方法. 这就是区别, 一个枚举只代表一个值, 但是声明体内, 能找到所有的状态, 所以可以在声明体内直接写一次方法, 而结构体每个实例都实现一遍.

  1. 如果你不拥有枚举源码, 那么你无法增加新的case, 而结构体的话, 你只需要新建一个, 并实现相应的协议即可.
  2. 即使你拥有枚举源码, 你增加了case也会破坏所有的旧代码(没有穷尽所有case)
  3. 而对于结构体, 你又是不那么容易添加新的协议方法(特别是也不拥有协议源码的情况下, 也不建议去扩展一个协议because they’re not dynamically dispatched.), 枚举则不存在这个问题.

各有优劣, 根据实际情况来建模你的数据结构吧.

6. Use enums to model recursive data structures

Enums are perfect for modeling recursive data structures, i.e. data structures that “contain” themselves, for example, trees: XML, JSON.

/// A singly linked list.
enum List<Element> {
    case end
    indirect case node(Element, next: List<Element>)
}
  1. 一个节点, 要么是end, 要么是node, 这是典型的Sum Type, 即不可能共存, 使用enum来建模挺好
  2. indirect关键字, 是把值类型当引用类型来用, 不然会发生复制.

如果不使用indirect关键字, 则可以把枚举实例用一个class包一下(这样使用的时候就必须要解包了):

final class Box<A> {
    var unbox: A
    init(_ value: A) { self.unbox = value }
}
enum BoxedList<Element> {
    case end
    case node(Element, next: Box<BoxedList<Element>>) // node变成了引用类型
}
  1. indirect enum List { ... }.这样写到外面也可以
  2. case node(Box<(Element, next: BoxedList<Element>)>)这样写也可以
let emptyList = List<Int>.end
let oneElementList = List.node(1, next: emptyList)
// node(1, next: List<Swift.Int>.end)

相比用structclass来建模, enum在表达"只有一种状态"的场景最准确, 而structclass则是可以建出很多不存在的场景的, 比如既有节点的值, 又标识这是叶节点(在另一种意义上这也是成立的, 即一定要通过是否叶节点这个状态来判断, 而不是像enum一样直接变成了另一种节点)

// 实现一个prepend方法
extension List {
    func cons(_ x: Element) -> List {
        return .node(x, next: self)
    }
}
// A 3-element list, of (3 2 1).
let list = List<Int>.end.cons(1).cons(2).cons(3)

就是生成一个新节点, 把next指向自己即可, 这里需要思路清晰, 你是为note在添加前置节点, 并不是为整个链表增加前置节点. 所以这会产生一个问题, 你还有别的节点的next也指向了当前节点.

使用实例是这样做的:

  1. 生成一个叶节点
  2. 为叶节点增加一个前置
  3. 如此往复

但是语法非常ugly, 可以更加literal(字面量)一点:

extension List: ExpressibleByArrayLiteral {
    public init(arrayLiteral elements: Element...) {
            self = elements.reversed().reduce(.end) { partialList, element in
            partialList.cons(element)
        }
    }
}
let list2: List = [3,2,1]

目前实现的是新建了一个链表, 下面演示直接改这个链表(我们知道其实没有这个表的存在, 因为它是靠recursive来实现的, 表现在外面就是一个node):

extension List {
    // 从代码来看, 这个push还是在前面拼接, 而不是尾部拼接
    mutating func push(_ x: Element) {
        self = self.cons(x) // 先生成一个新的node, 然后定义self为新的node
    }
    mutating func pop() -> Element? {
        switch self {
            case .end: return nil
            case let .node(x, next: tail): // 把自己移除了, 它的next则成了自己
                self = tail
                return x
        }
    }
}

既然只是一个node, 所以每次都是把self重新定义为新的node, 这样就实现了修改(with mutating).

Raw Values

The RawRepresentable Protocol

A raw-representable type gains two new APIs:

  1. a rawValue property
  2. and a failable initializer (init?(rawValue:)).
/// A type that can be converted to and from an associated raw value.
protocol RawRepresentable {
    /// The type of the raw values, such as Int or String.
    associatedtype RawValue
    init?(rawValue: RawValue)
    var rawValue: RawValue { get }
}

Manual RawRepresentable Conformance

并不是只有数字和字符串能作为raw value, 你可以自定义, 但是需要遵循RawRepresentable协议, 并实现rawValueinit(rawValue:)两个方法.

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

AnchorPoint.topLeft.rawValue // (x: -1, y: 1)
AnchorPoint(rawValue: (x: 0, y: 0)) // Optional(AnchorPoint.center)
AnchorPoint(rawValue: (x: 2, y: 1)) // nil

And it’s exactly the code the compiler generates for us in its automatic RawRepresentable synthesis.

  • Switching over an enum always matches against the enum’s cases and never the raw values.
  • In other words, you can’t match one case with another case, even if they have the same raw value.

RawRepresentable for Structs and Classes

假定你要为一个字符串字段实现一个新类型, 以区别其它字符串:

struct UserID: RawRepresentable {
    var rawValue: String
}

let userID: UserID = UserID(rawValue: "1234")
print(userID.rawValue) // "1234"

它满足了RawRepresentable协议中的rawValue, 但是它没有init(rawValue:)方法, 因为Swift的结构体默认含有所有属性的初始化方法(比enum还方便).

Internal Representation of Raw Values

不同的RawType并没有改变它是一个枚举的特性(既不会变成一个Int也不会变成一个String)

enum MenuItem: String {
case undo = "Undo"
case cut = "Cut"
case copy = "Copy"
case paste = "Paste"
}
MemoryLayout<MenuItem>.size // 1 byte
MemoryLayout<Int>.size // 8
MemoryLayout<String>.size // 16

Enumerating Enum Cases

The CaseIterable protocol models this functionality by adding a static property (i.e. a property that’s invoked on the type, not on an instance) named allCases:

/// A type that provides a collection of all of its values.
protocol CaseIterable {
    associatedtype AllCases: Collection
        where AllCases.Element == Self
    static var allCases: AllCases { get }
}
  • For enums without associated values, the compiler can automatically generate a CaseIterable implementation;
  • all we have to do is declare the conformance.
  • 文档保证了allCases的顺序和enum定义的顺序一致.
  • 自动生成比手写的好处是它永远是最新的, 不会因为添加了新的case而遗漏
  • compiler-synthesized protocol implementations 自动给CaseIterable实现了EquatableHashable协议, 所以你不需要手动实现.
    • 同样, 也会随着你对枚举的修改而自动更新
    • 带关联数据的枚举则不会自动实现, 因为关联数据的不同可能让枚举拥有无穷尽的状态

Manual CaseIterable Conformance

一些自定义的实现:

extension Bool: CaseIterable {
    public static var allCases: [Bool] {
        return [false, true]
    }
}
Bool.allCases // [false, true]

// 可以是一个Collection, 这样可以节省很多空间(没必要真做一个数组)
extension UInt8: CaseIterable {
	public static var allCases: ClosedRange<UInt8> {
		return .min ... .max
	}
}
UInt8.allCases.count // 256
UInt8.allCases.prefix(3) + UInt8.allCases.suffix(3) // [0, 1, 2, 253, 254, 255]

这个思路可以用在带关联数据的枚举上, 可以不去增加无穷无尽的成员以实现allCases

We’ll discuss lazy collections in the Collection Protocols chapter.

Frozen and Non-Frozen Enums

  • An enum that may gain new cases in the future is called non-frozen. This is the default.
  • code that switches over a non-frozen enum from another module must always include a default clause to be able to handle future cases.

如果你让编辑器帮你fix, 它会自动加上一个unknown:

switch error {
    ...
    case .dataCorrupted: ...
    @unknown default:
    // Handle unknown cases.
    ...
}

这是一个强烈的信号, 即你已经穷尽了当前组合, 这个default是进不来的, 或者说, 能进来的是当前代码环境里不知道的case.

  • 意思是如果你当前的代码里default匹配中了任何一个case, 编辑器仍然会给你一个警告.
  • Examples of frozen enums in the standard library include Optional and Result
    • 不然你在使用的地方永远要跟一个default

最佳实践

  1. 避免嵌套的case, 比如两个指标: 重要和紧急, 进行如下定义:
let isImportant: Bool = ...
let isUrgent: Bool = ...
let priority: Int
switch isImportant {
    case true:
        switch isUrgent {
            case true: priority = 3
            case false: priority = 2
        }
    case false:
        switch isUrgent {
            case true: priority = 1
            case false: priority = 0
        }
}

可以这么做, 元组是可以switch的:

let priority2: Int
switch (isImportant, isUrgent) {
    case (true, true): priority2 = 3
    case (true, false): priority2 = 2
    case (false, true): priority2 = 1
    case (false, false): priority2 = 0
}
  1. 当工厂方法用:
enum OpaqueColor {
    case rgb(red: Float, green: Float, blue: Float)
    case gray(intensity: Float)
}

首先,

OpaqueColor.rgb // (Float, Float, Float) -> OpaqueColor

其次:

let gradient = stride(from: 0.0, through: 1.0, by: 0.25).map(OpaqueColor.gray)
/*
[OpaqueColor.gray(intensity: 0.0), OpaqueColor.gray(intensity: 0.25),
OpaqueColor.gray(intensity: 0.5), OpaqueColor.gray(intensity: 0.75),
OpaqueColor.gray(intensity: 1.0)]
*/
  1. Use caseless enums as namespaces.
  • Swift doesn’t have namespaces built in.
  • Since type definitions can be nested
    • outer types act as namespaces for all declarations they contain.

Backlinks