Advanced Swift 6...

Advanced-Swift-Sample-Code

6. 编码和解码

概览

/// 某个类型可以将⾃身编码为⼀种外部表示。

public protocol Encodable { 
  /// 将值编码到给定的 encoder 中。 
  public func encode(to encoder: Encoder) throws
} 

/// 某个类型可以从外部表示中解码得到⾃身。 
public protocol Decodable { 
  /// 通过从给定的 decoder 中解码来创建新的实例例。 
  public init(from decoder: Decoder) throws
} 

public typealias Codable = Decodable & Encodable 

编码器和解码器的核心任务是管理那些用来存储序列后的数据的容器的层次

自动遵循协议

struct Coordinate: Codable {
  var latitude: Double
  var longitude: Double
  //不需要实现
}

struct Placemark: Codable {
  var name: String
  var coordinate: Coordinate
}

代码生成和 “普通” 的默认实现形式上来说唯一的不同在于,默认实现是标准库的一部分,而 Codable 代码的合成是存在于编译器中的。

Encoding

<1> JSONEncoder & PropertyListEncoder 对于满足 Codable 的类型,它们也将自动适配 Cocoa 的 NSKeyedArchiver。

<2> 除了通过一个属性来设定输出格式 (带有缩进的易读格式和/或按词典对键进行排序) 以外, JSONEncoder 还支持对于日期 (包括 ISO 8601 或者 Unix epoch 时间戳),Data 值 (比如 Base64 方式) 以及如何在发生异常时处理浮点值 (作为无限或是 not a number) 进行自定义。

<3> 事实上,JSONEncoder 甚至都没有实现 Encoder 协议。它只是一个叫做 _JSONEncoder 的私有类的封装,这个类实现了 Encoder 协议,并且进行实际的编码工作。这么设计的原因是,顶层的编码器应该提供的是完全不同的 API (或者说,提供一个方法来开始编码的过程),而不是一个在编码过程中用来传递给 codable 类型的 Encoder 对象。将这些任务清晰地分解开,意味着在任意给定的情景下,使用编码器的一方只能访问到适当的 API。比如,因为公开的配置 API 只暴露在顶层编码器的定义中,所以一个 codable 类型不能在编码过程中重新对编码器进行配置。

Decoding

但是 Swift 团队还是决定增加 API 的明确性,避免产生歧义要比最大化精简代码要来得重要

你可以查看 DecodingError 类型的文档来确认你可能会遇到哪些错误。

编码过程

当你开始编码过程时,编码器会调用正在被编码的值上的 encode(to: Encoder) 方法,并将编码器自身作为参数传递进去。接下来,值需要将自己以合适的格式编码到编码器中。

places.encode(to: self)

容器

显然 Encoder 的核心功能就是提供一个编码容器。容器是编码器存储的一种沙盒表现形式。通过为每个要编码的值创建一个新的容器,编码器能够确保每个值都不会覆盖彼此的数据。

容器有三种类型:

<1> 键容器(KeyedContainer)可以对键值对进行编码。将键容器想像为一个特殊的字典, 到现在为止,这是最常⻅的容器。在基于键的编码容器中,键是强类型的,这为我们提供了类型安全和自动补全的特性。 编码器最终会在写入目标格式 (比如 JSON) 时,将键转换为字符串 (或者数字),不过这对开发者来说是隐藏的。自定义编码方式的最简单的办法就是更改你的类型所提供的键。

<2> 无键容器(UnkeyedContainer)将对一系列值进行编码,而不需要对应的键,可以将它想像成被编码值的数组。因为没有对应的键来确定某个值,所以对于在容器中的值进行解码的时候,需要遵守和编码时同样的顺序。

<3> 单值容器对一个单一值进行编码。你可以用它来处理整个数据被定义为单个属性的那类类型。单值容器应用的例子包括像是 Int 这样的原始类型,或者是底层由原始类型的 RawRepresentable 所表达的枚举值类型。

对于这三种容器,它们每个都对应了一个协议,来定义容器应当如何接收一个值并进行编码。

SingleValueEncodingContainer & UnkeyedEncodingContainer & KeyedEncodingContainerProtocol

其他不属于基础类型的值,最后都会落到泛型的 encode<T: Encodable> 重载中。在这个方法里,容器最终会调用参数的 encode(to: Encoder) 方法,整个过程会向下一个层级并重新开始,最终到达只剩下原始类型的情况。不过容器在处理具体类型时,可以有自身的不同的特殊要求。

值是如何对自己编码的

Array<Placemark> - 数组将会向编码器请求一个无键容器

编译器生成的代码

Coding Keys

<1> 这个枚举包含的成员与结构体中的存储属性一一对应。枚举值即为键编码容器所使用的键。

和字符串的键相比较,因为有编译器检查拼写错误,所以这些强类型的键要更加安全,也更加方便。不过,编码器最后为了存储需要,还是必须要能将这些键转为字符串或者整数值。

private enum CodingKeys: CodingKey {
  case name
  case coordinate
}

<2> CodingKey 协议会负责这个转换任务:

/// 该类型作为编码和解码时使⽤的键

public protocol CodingKey {

   /// 在⼀个命名集合 (⽐如一个字符串作为键的字典) 中的字符串值。
  var stringValue: String { get }

  /// 在⼀个整数索引集合 (⽐如⼀个整数作为键的字典) 中使⽤的值。
  var intValue: Int? { get }

  init?(stringValue: String)
  init?(intValue: Int)
}

// encode(to:) 方法
struct Placemark3: Codable {

 // ...
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(coordinate, forKey: .coordinate)
  }
}

// init(from:) 初始化方法 
struct Placemark: Codable {
  // ...
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    coordinate = try container.decode(Coordinate.self, forKey: .coordinate)
  }
}

手动遵守协议

自定义 Coding Keys

<1> 使用明确给定的字符串值,在编码后的输出中重命名字段

<2> 将某个键从枚举中移除,以此完全跳过字段。

struct Placemark3: Codable {
  var name: String
  var coordinate: Coordinate

  private enum CodingKeys: String, CodingKey {
    case name = "label"
    case coordinate
 }
 // 编译器器⽣成的编码和解码⽅法将使用重载后的 CodingKeys
}

struct Placemark4: Codable {
  var name: String = "(Unknown)" // 默认值
  var coordinate: Coordinate

  private enum CodingKeys: CodingKey {
    case coordinate
  }
}

自定义的 encode(to:)init(from:) 实现

JSONEncoder 和 JSONDecoder 默认就可以处理可选值。如果目标类型中的一个属性是可选值,那么输入数据中对应的值不存在的话,解码器将会正确地跳过这个属性。

常见的编码任务

让其他人的代码满足 Codable

<1> Swift 4.0 只会对那些在类型被定义的同时,就指定满足 Codable 的类型生成代码。

<2> 嵌套容器

KeyedDecodingContainer 有一个叫做 nestedContainer(keyedBy:forKey:) 的方法,它能够 (使用另一套编码键类型) 另外创建一个键容器。

<3> 使用计算属性绕开问题

不过我们给用戶暴露一个 CLLocationCoordinate2D 的计算属性。

由于它是一个计算属性, Codable 系统会将它忽略掉。

让类满足 Codable

<1> 要让这个动态派发正确工作,编译器需要在类的派发表中为该初始化方法创建一个条目。该类的非 final 方法所对应的表是在类的定义被编译的时候进行创建的,在创建的时候它的大小就固定了;扩展不能再向其中添加新的条目。这就是为什么 required 初始化方法只能在类定义中存在的原因。

<2> 在 Swift 4 中,我们不能为一个非 final 的类添加 Codable 特性。

<3> 推荐的方式是写一个结构体来封装 UIColor,并且对这个结构体进行编解码。 - 颜色空间没有包含

<4> 封装结构体的方式最大的缺点在于,你需要手动在编码前和解码后将类型在 UIColor 和封装类型之间进行转换。

<5> 计算属性

让枚举满足 Codable

<1> 当枚举类型满足 RawRepresentable 协议,并且 RawValue 类型是 “原生” 的几个 codable 类型 (也就是说,Bool,String,Float,Double 或者任一整数类型) 时,编译器会为枚举类型提供 Codable 的代码生成。

<2> 对于其他像是带有关联值的枚举,你需要手动进行实现才能满足 Codable - enum Either<A: Codable, B: Codable>: Codable

解码多态集合

怎么才能对这样的多态对象集合进行编码呢?最好的方式是对每个我们想要支持的子类创建一个枚举成员。枚举的关联值中存储的是实际的对象

→ 在编码过程中,对要编码的对象在所有枚举成员上做switch来找到我们要编码的类型。 然后将对象的类型和对象本身编码到它们的键中。

→ 在解码过程中,先解码类型信息,然后根据具体的类型选择合适的初始化方法。

7. 函数

<1> 函数可以被赋值给变量,也能够作为函数的输入和输出

<2> 函数能够捕获存在于其局部作用域之外的变量。

<3> 有两种方法可以创建函数,一种是使用 func 关键字,另一种是 {}。在Swift中,后一种被称为闭包表达式。

在编程术语里,一个函数和它所捕获的变量环境组合起来被称为闭包。

使用闭包表达式来定义的函数可以被想成函数的字面量,与 func 相比较,它的区别在于闭包表达式是匿名的,它们没有被赋予一个名字。

extension BinaryInteger {
  var isEven: Bool { return self % 2 == 0 }
}

func isEven<T: BinaryInteger>(_ i: T) -> Bool {
  return i % 2 == 0
}

记住,闭包指的是一个函数以及被它所捕获的所有变量的组合。而使用 { } 来创建的函数被称为闭包表达式,人们常常会把这种语法简单地叫做闭包。

函数的灵活性

实际上一共有四个排序的方法:不可变版本的 sorted(by:) 和可变的 sort(by:),以及两者在待排序对象遵守 Comparable 时进行升序排序的无参数版本

排序描述符用到了 Objective-C 的两个运行时特性:

<1> 首先,key 是 Objective-C 的键路径,它其实是一个包含属性名字的链表。不要把它和 Swift 4 引入的原生的 (强类型的) 键路径搞混。

<2> 第二个 Objective-C 运行时特性是键值编程 (key-value-coding),它可以在运行时通过键查找一个对象上的对应值。selector 参数接受一个 selector (实际上也是一个用来描述方法名字的字符串),在运行时,这个 selector 将被用来查找比较函数,当对两个对象进行比较时,这个比较函数将被用来对指定键所对应的值进行比较。

函数作为数据

<1> 实现 SortDescriptor

这种方式的实质是将函数用作数据,我们将这些函数存储在数组里,并在运行时构建这个数组。这将动态特性带到了一个新的高度,这也是像 Swift 这样的编译时就确定了静态类型的语言仍然能实现像是 Objective-C 或者 Ruby 的部分动态行为的一种方式。

<2> 自定义的运算符,来合并两个排序函数

<3> 处理可选值

func lift<A>(_ compare: @escaping (A) -> (A) -> ComparisonResult) -> (A?) -> (A?) -> ComparisonResult

<4> 这样的做法也让我们能够清晰地区分排序方法和比较方法的不同。Swift 的排序算法使用的是多个排序算法的混合。

在写这本书的时候,排序算法基于的是内省排序 (introsort),而内省排序本身其实是快速排序和堆排序的混合。但是,当集合很小的时候,会转变为插入排序(insertion sort),以避免那些更复杂的排序算法所需要的显著的启动消耗。

局部函数和变量捕获

merge 排序

函数作为代理

Foundation 框架的代理

结构体代理

当我们给 alert.delegate 赋值的时候,Swift 将结构体进行了复制。

在代理和协议的模式中,并不适合使用结构体。

使用函数,而非代理

<1> 函数类型只能有一个明确写出的空类型标签,配合上一个内部的参数名字,而不能拥有独立的参数标签。它能让我们给函数类型的参数一个标签,用来作为进行文档说明。在 Swift 支持一种更好的方式之前,这是官方所认可的变通方式。

<2> 函数回调 - struct

<3> 函数回调 - class - 循环引用

<4> 要注销一个代理或者函数回调,我们可以简单地将它设为 nil。但如果我们的类型是用一个数组来存储代理或者回调呢?

对于基于类的代理,我们可以直接将它从代理列表中移除; 不过对于回调函数,就没那么简单了,因为函数不能被比较,所以我们需要添加额外的逻辑去进行移除。

inout 参数和可变方法

<1> inout 做的事情是通过值传递,然后复制回来,而并不是传递引用。

<2> lvalue 描述的是一个内存地址,它是 “左值 (left value)” 的缩写,因为 lvalues 是可以存在于赋值语句左侧的表达式。举例来说,array[0] 是一个 lvalue,它代表的是数组中第一个元素所在的内存值。而 rvalue 描述的是一个值。2 + 2 是一个 rvalue,它描述的是 4 这个值。你不能把 2 + 2 或者 4 放到赋值语句的左侧。

<3> 对于 inout 参数,你只能传递 lvalue 给他,因为我们不可能对一个 rvalue 进行改变。

嵌套函数和 inout

& 不意味 inout 的情况 - UnsafeMutablePointer

计算属性

有两种方法和其他普通的方法有所不同,那就是计算属性和下标方法。计算属性看起来和常规的属性很像,但是它并不使用任何内存来存储自己的值。

相反,这个属性每次被访问时,返回值都将被实时计算出来。下标的话,就是一个遵守特殊的定义和调用规则的方法。

观察变更

<1> 属性观察者必须在声明一个属性的时候就被定义,你无法在扩展里进行追加。所以,这不是一个提供给类型用戶的工具,它是专⻔为类型的设计者而设计的。

<2> willSet 和 didSet 本质上是一对属性的简写:一个负责为值提供存储的私有存储属性,以及一个公开的计算属性。这个计算属性的 setter 会在将值存储到存储属性中之前和/或之后,进行额外的工作。

<3> 这和 Foundation 中的键值观察有本质的不同,键值观察通常是对象的消费者来观察对象内部变化的手段,而与类的设计者是否希望如此无关。

<4> 你可以在子类中重写一个属性,来添加观察者。

<5> KVO 使用 Objective-C 的运行时特性, 它动态地在类的 setter 中添加观察者,这在现在的 Swift 中,特别是对值类型来说,是无法实现的。Swift 的属性观察是一个纯粹的编译时特性。

延迟存储属性 - lazy

<1> 访问一个延迟属性是 mutating 操作,因为这个属性的初始值会在第一次访问时被设置。
当结构体包含一个延迟属性时,这个结构体的所有者如果想要访问该延迟属性的话,也需要将结构体声明为可变量,因为访问这个属性的同时,也会潜在地对这个属性的容器进行改变。

<2> 让想访问这个延迟属性的所有 Point 用戶都使用 var 是非常不方便的事情,所以在结构体中使用延迟属性通常不是一个好主意。

<3> “behaviors”

下标

下标进阶

在 Swift 4 中,下标还可以在参数或者返回类型上使用泛型。

键路径

<1> 键路径是一个指向属性的未调用的引用,它和对某个方法的未使用的引用很类似。- \String.count - .count

<2> 键路径可以由任意的存储和计算属性组合而成,其中还可以包括可选链操作符。编译器会自动为所有类型生成 [keyPath:] 的下标方法。你通过这个方法来 “调用” 某个键路径。对键路径的调用,也就是在某个实例上访问由键路径所描述的属性。所以,"Hello"[keyPath: .count] 等效于 "Hello".count。

可以通过函数建模的键路径

相对于这样的函数,键路径除了在语法上更简洁外,最大的优势在于它们是值。你可以测试键路径是否相等,也可以将它们用作字典的键 (因为它们遵守 Hashable)。

另外,不像函数,键路径是不包含状态的,所以它也不会捕获可变的状态。

可写键路径

设想你要将两个属性互相绑定:当属性 1 发生变化的时候,属性 2 的值会自动更新,反之亦然。 可写的键路径在这种数据绑定的过程中会特别有用。

observe(_:options:changeHandler:) + @objc dynamic

键路径层级

→ AnyKeyPath 和 (Any) -> Any? 类型的函数相似

→ PartialKeyPath<Source> 和 (Source) -> Any? 函数相似

→ KeyPath<Source, Target> 和 (Source) -> Target 函数相似

→ WritableKeyPath<Source, Target> 和 (Source) -> Target 与 (inout Source, Target) -> () 这一对函数相似

→ ReferenceWritableKeyPath<Source, Target> 和 (Source) -> Target 与 (Source, Target) -> () 这一对函数相似。

第二个函数可以用 Target 来更新 Source 的值,且要求 Source 是一个引用类型。对 WritableKeyPath 和 ReferenceWritableKeyPath 进行区分是必要的,前一个类型的 setter 要求它的参数是 inout 的。

对比 Objective-C 的键路径

自动闭包

短路求值

func and(_ l: Bool, _ r: @autoclosure () -> Bool) -> Bool {
  guard l else { return false }
  return r()
}

if and(!evens.isEmpty, evens[0] > 10) {
  //执行操作
}

在 Swift 标准库中,assert 和 fatalError 也使用了 @autoclosure,因为它们只在确实需要时才对参数进行求值。

自动闭包在实现日志函数的时候也很有用。 - 调试标识符

func log(ifFalse condition: Bool, message: @autoclosure () -> (String), file: String = #file, function: String = #function, line: Int = #line)
{
  guard !condition else { return }
  print("Assertion failed: \(message()), \(file):\(function) (line \(line))")
}

@escaping 标注

withoutActuallyEscaping

.lazy - 使用延迟的方式进行这些操作的目的是,我们可以在找到第一个不匹配的条目时就立即停止。

它可以让你把一个非逃逸闭包传递给一个期待逃逸闭包作为参数的函数。

8. 字符串

Unicode,而非固定宽度

今天的 Unicode 是一个可变⻓格式。它的可变⻓特性有两种不同的意义:由编码单元 (code unit) 组成 Unicode 标量 (Unicode scalar); 由 Unicode 标量组成字符。

字位簇和标准等价
合并标记

如果你真要进行标准的比较,你必须使用 NSString.compare(_:)。

颜文字

unicodeScalars

比如,NSString 有一个 enumerateSubstrings 方法,能被用来以字位簇的方式枚举字符串。

字符串和集合

String 是 Character 值的集合。

由字符组成的集合被移动到了 characters 属性里,它和 unicodeScalars,utf8 以及 utf16 等其他集合视图类似,是一种字符串的表现形式。

Swift 4 里,String 又成为了 Collection。characters 视图依然存在,但是仅仅是为了代码的前向兼容。

双向索引,而非随机访问

String 只实现了 BidirectionalCollection

extension String {
  var allPrefixes2: [Substring] {
    return [""] + self.indices.map { index in self[...index] }
  }
}

hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]

范围可替换,而非可变

String 还满足 RangeReplaceableCollection 协议。

var greeting = "Hello, world!"
if let comma = greeting.index(of: ",") {
  greeting[..<comma] // Hello
  greeting.replaceSubrange(comma..., with: " again.")
}

greeting // Hello again.

就算你想要更改的元素只有一个,你也必须使用 replaceSubrange。

字符串索引

下标访问:因为整数的下标访问无法在常数时间内完成 (对于 Collection 协议来说这也是个直观要求),而且查找第 n 个 Character 的操作也必须要对它之前的所有字节进行检查。

String.Index 是 String 和它的视图所使用的索引类型,它本质上是一个存储了从字符串开头的字节偏移量的不透明值。

index(after:) & index(_:offsetBy:) & limitedBy:

子字符串

它是一个以不同起始和结束索引的对原字符串的切片。

extension Collection where Element: Equatable {
  public func split(separator: Element, maxSplits: Int = Int.max, omittingEmptySubsequences: Bool = true) -> [SubSequence] 
}

这个函数和 String 从 NSString 继承来的 components(separatedBy:) 很类似,不过还多加了一个决定是否要丢弃空值的选项。

StringProtocol

// 从后向前进行迭代,直到我们找到第一个分隔符,会是更好的策略。 
func lastWord(in input: String) -> String? {
  // 处理输⼊,操作⼦字符串
  let words = input.split(separators: [",", " "])
  guard let lastWord = words.last else { return nil }

 // 转换为字符串并返回 - 和所有的切片一样,子字  符串也只能用于短期的存储,这可以避免在操作过程中发生昂贵的复制。 
  return String(lastWord)
}

lastWord(in: "one, two, three, four, five") // Optional("five")

不鼓励⻓期存储子字符串的根本原因在于,子字符串会一直持有整个原始字符串。如果有一个巨大的字符串,它的一个只表示单个字符的子字符串将会在内存中持有整个字符串。

⻓期存储子字符串实际上会造成内存泄漏,由于原字符串还必须被持有在内存中,但是它们却不能再被访问。

extension Sequence where Element: StringProtocol {
  /// 将⼀个序列中的元素使⽤给定的分隔符拼接起为新的字符串,并返回 
  public func joined(separator: String = "") -> String 
} 

let commaSeparatedNumbers = "1,2,3,4,5"
let numbers = commaSeparatedNumbers.split(separator: ",").flatMap { Int($0) } 
// [1, 2, 3, 4, 5]

StringProtocol 设计之初就是为了在你想要对 String 扩展时来使用的。

编码单元视图

String 为此提供了三种视图: unicodeScalars,utf16 和 utf8,和 String 一样,它们是双向索引的集合,并且支持所有我们已经熟悉了的操作。

非随机访问

共享索引

字符串和它们的视图共享同样的索引类型,String.Index。

字符串 和 Foundation

标准库中的 split 方法和 Foundation 里的 components(separatedBy:)。

另外还有很多其他不匹配的地方: Foundation 使用 ComparisonResult 来表示比较断言的结果,而标准库是围绕布尔值来设计断言的;

像是 trimmingCharacters(in:) 和 components(separatedBy:) 接受一个 CharacterSet 作为参数, 而很不幸,CharacterSet 这个类型的名字在 Swift 中是相当不恰当的。 enumerateSubstrings(in:options:_:) 这个使用字符串和范围来对输入字符串按照字位簇、单词、句子或者段落进行迭代的超级强力的方法,在 Swift 中对应的 API 使用的是子字符串。

其他基于字符串的 Foundation API

用来查询给定位置的格式属性的 attributes(at: Int, effectiveRange: NSRangePointer?) 方法,接受的就是一个 (以 UTF-16 测量的) 整数索引,而非 String.Index,它通过指针返回的 effectiveRange 是一个 NSRange,而非 Range<String.Index>。

你传递给 NSMutableAttributedString.addAttribute(_:value:range:) 方法的范围也遵循同样的规则。

字符范围

… - 不可数

extension Unicode.Scalar: Strideable {
  public typealias Stride = Int
  public func distance(to other: Unicode.Scalar) -> Int {
    return Int(other.value) - Int(self.value) 
  }

  public func advanced(by n: Int) -> Unicode.Scalar {
    return Unicode.Scalar(UInt32(Int(value) + n))!
  }
}

let lowercase = ("a" as Unicode.Scalar)..."z"
Array(lowercase.map(Character.init))
/*
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
*/

CharacterSet

因为它确实就是一个表示一系列 Unicode 标量的数据结构体。它完全和 Character 类型不兼容。

String 和 Character 的内部结构

从 Objective-C 接收到的字符串背后是一个 NSString。在这种时候,为了让桥接尽可能高效, NSString 直接扮演了 Swift 字符串的缓冲区。一个基于 NSString 的 String 在被改变时,将会 被转换为原生的 Swift 字符串。

Character 类型

Character源码。

简单的正则表达式匹配器

ExpressibleByStringLiteral

字符串字面量隶属于 ExpressibleByStringLiteral、 ExpressibleByExtendedGraphemeClusterLiteral 和 ExpressibleByUnicodeScalarLiteral 这三个层次结构的协议。

extension Regex: ExpressibleByStringLiteral {
  public init(stringLiteral value: String) {
    regexp = value
  }
}
let r: Regex = "^h..lo*!$" 

CustomStringConvertibleCustomDebugStringConvertible

文本输出流

public func print<Target: TextOutputStream>(_ items: Any..., separator: String = " ", terminator: String = "\n", to output: inout Target) 

TextOutputStream

TextOutputStreamable

let toReplace: DictionaryLiteral<String, String> 

上面的代码中,我们使用了 DictionaryLiteral 而不是一个普通的字典。

Dictionary 有两个副作用: 它会去掉重复的键,并且会将所有键重新排序。

如果你想要使用像是 [key: value] 这样的字面量语法,而又不想引入 Dictionary 的这两个副作用的话,就可以使用 DictionaryLiteral。

DictionaryLiteral 是对于键值对数组 (比如 [(key, value)]) 的很好的替代,它不会引入字典的副作用,同时让调用者能够使用更加便捷的 [:] 语法。

字符串性能

幻影 (phantom) 类型

9. 错误处理

除了调用者必须处理成功和失败的情况的语法以外,和可选值相比,能抛出异常的方法的主要区别在于,它可以给出一个包含所发生的错误的详细信息的值。

Result 类型

抛出和捕获

注意 Result 是作用于类型上的,而 throws 作用于函数。

关键字 try 的目的有两个: 首先,对于编译器来说这是一个信号,表示我们知道我们将要调用的函数可能抛出错误。更重要的是,它让代码的读者知道代码中哪个函数可能会抛出。

通过调用一个可抛出的函数,编译器迫使我们去考虑如何处理可能的错误。我们可以选择使用 do/catch 来处理错误,或者把当前函数也标记为 throws,将错误传递给调用栈上层的调用者。 如果使用 catch 的话,我们可以用模式匹配的方式来捕获某个特定的错误或者所有错误。

Swift 的错误抛出其实是无类型的

带有类型的错误

func parse(text: String) -> Result<[String], ParseError> 

将错误桥接到 Objective-C

错误和函数参数

Rethrows

rethrows 告诉编译器,这个函数只会在它的参数函数抛出错误的时候抛出错误。对那些向函数中传递的是不会抛出错误的 check 函数的调用,编译器可以免除我们一定要使用 try 来进行调用的要求。

使用 defer 进行清理

比如你想将代码的初始化工作和在关闭时对资源的清理工作放在一起时,就可以使用 defer。 逆序执行

错误和可选值

我们可以使用 try? 来忽略掉 throws 函数返回的错误,并将返回结果转换到一个可选值中,来告诉我们函数调用是否成功

if let result = try? parse(text: input) { 
  print(result) 
} 

错误链

我们只需要简单地将这些函数调用放到一个 do/catch 代码块中 (或者封装到一个被标记为 throws 的函数中) 去。

链结果

extension Result {
  func flatMap<B>(transform: (A) -> Result<B>) -> Result<B> {
    switch self {
    case let .failure(m): return .failure(m)
    case let .success(x): return transform(x)
    }
  }
}

高阶函数和错误

Result 是异步错误处理的正确道路。

不好的地方在于,如果你已经在同步函数中使用 throws 了,再在异步函数中转为使用 Result 将会在两种接口之间导入差异。

当我们不能继续运行代码时,可以选择使用 fatalError 或者是断言。当我们对错误类型不感兴趣,或者只有一种错误时,我们使用可选值。当我们需要处理多种错误,或是想要提供额外的信息时,可以使用 Swift 内建的错误,或者是自定义一个 Result 类型。当我们想要写一个接受函数的函数时,我们可以使用 rethrows 来让这个待写函数同时接受可抛出和不可抛出的函数参数。最后,defer 语句在结合内建的错误处理时非常有用。defer 语句为我们提供了集中放置清理代码的地方,不论是正常退出,还是由于错误而被中断,defer 语句所定义的代码块都将被执行并进行清理。

10. 泛型

泛型编程的目的是表达算法或者数据结构所要求的核心接口。

通过确认核心接口到底是什么,也就是说,找到想要实现的功能的最小需求,我们可以将这个函数定义在宽阔得多的类型范围内。

重载

拥有同样名字,但是参数或返回类型不同的多个方法互相称为重载方法

自由函数的重载

func log<View: UIView>(_ view: View) {
  print("It's a \(type(of: view)), frame: \(view.frame)")
}

func log(_ view: UILabel) {
  let text = view.text ?? "(empty)"
  print("It's a label, text: \(text)")
}

运算符的重载

对于重载的运算符,类型检查器会去使用非泛型版本的重载,而不考虑泛型版本。

使用泛型约束进行重载

实际上 isSubset 并不需要这么具体,在两个版本中只有两个函数调用,那就是两者中都有的 contains 以及 Hashable 版本中的 Set.init。

使用闭包对行为进行参数化

我们可以要求调用者提供一个函数来表明元素相等的意义,这样一来,我们就把判定两个元素相等的控制权交给了调用者。

对集合采用泛型操作

二分查找

泛型二分查找

集合随机排列

for i in indices.dropLast() 

如果 indices 属性持有了对集合的引用,那么在遍历 indices 的同时更改集合内容,将会让我们失去写时复制的优化,因为集合需要进行不必要的复制操作。

使用泛型进行代码设计

提取共通功能

创建泛型数据类型

泛型的工作方式

// → 编译器不知道(包括参数和返回值在内的)类型为 T 的变量的大小

// → 编译器不知道需要调用的 < 函数是否有重载,因此也不知道需要调用的函数的地址。

对于每个泛型类型的参数,编译器还维护了一系列一个或者多个所谓的目击表 (witness table): 其中包含一个值目击表,以及类型上每个协议约束一个的协议目击表。这些目击表 (也被叫做 vtable) 将被用来将运行时的函数调用动态派发到正确的实现去。

泛型特化

泛型特化是指,编译器按照具体的参数参数类型 (比如 Int),将 min<T> 这样的泛型类型或者函数进行复制。

全模块优化

Swift 中有一个叫做 @_specialize 的非官方标签,它能让你将你的泛型代码进行指定版本的特化,使其在其他模块中也可用。

另外还有一个相似 (同样非官方支持) 的 @_inlineable 标签,当构建代码时,它指导编译器将被标记函数的函数体暴露给优化器。这样,跨模块的优化壁垒就被移除了。

相比 @_specialize, @_inlineable 的优势在于,原来的模块不需要将具体类型硬编码成一个列表,因为特化会在使用者的模块进行编译时才被施行。

11. 协议

Swift 的协议和 Objective-C 的协议不同。Swift 协议可以被用作代理,也可以让你对接口进行抽象 (比如 IteratorProtocol 和 Sequence)。

它们和 Objective-C 协议的最大不同在于我们可以让结构体和枚举类型满足协议。除此之外,Swift 协议还可以有关联类型。我们还可以通过协议扩展的方式为协议添加方法实现。

普通的协议可以被当作类型约束使用,也可以当作独立的类型使用。

带有关联类型或者 Self 约束的协议特殊一些: 我们不能将它当作独立的类型来使用,所以像是 let x: Equatable 这样的写法是不被允许的; 它们只能用作类型约束,比如 func f<T: Equatable>(x: T)。

不过在 Swift 中,Sequence 中的代码共享通过协议和协议扩展来实现的。通过这么做, Sequence 协议和它的扩展在结构体和枚举这样的值类型中依然可用,而这些值类型是不支持子类继承的。

协议扩展是一种可以在不共享基类的前提下共享代码的方法。

子类必须知道哪些方法是它们能够重写而不会破坏父类行为的。

面向协议编程

协议的最强大的特性之一就是我们可以以追溯的方式来修改任意类型,让它们满足协议。

协议扩展

Swift 的协议的另一个强大特性是我们可以使用完整的方法实现来扩展一个协议。

通过协议进行代码共享相比与通过继承的共享,有这几个优势:

→ 我们不需要被强制使用某个父类。

→ 我们可以让已经存在的类型满足协议(比如我们让 CGContext 满足了 Drawing)。子类就没那么灵活了,如果 CGContext 是一个类的话,我们无法以追溯的方式去变更它的父类。

→ 协议既可以用于类,也可以用于结构体,而父类就无法和结构体一起使用了。

→ 最后,当处理协议时,我们无需担心方法重写或者在正确的时间调用super这样的问题。

在协议扩展中重写方法

协议要求的方法是动态派发的,而仅定义 在扩展中的方法 是静态派发的。

var otherSample: Drawing = SVG()

当我们将 otherSample 定义为 Drawing 类型的变量时,编译器会自动将 SVG 值封装到一个代表协议的类型中,这个封装被称作存在容器 (existential container)。

协议的两种类型

对于 Sequence 需要实现哪些方法,标准库在 “满足 Sequence 协议” 的部分进行了文档说明。

类型抹消

<1> 从这次重构中,我们可以总结出一套创建类型抹消的简单算法。

首先,我们创建一个名为 AnyProtocolName 的结构体或者类。

然后,对于每个关联类型,我们添加一个泛型参数。

接下来,对于协议的每个方法,我们将其实现存储在 AnyProtocolName 中的一个属性中。

最后,我们添加一个将想要抹消的具体类型泛型化的初始化方法; 它的任务是在闭包中捕获我们传入的对象,并将闭包赋值给上面步骤中的属性。

<2> 标准库采用了一种不同的策略来处理类型抹消: 它使用了类继承的方式,来把具体的迭代器类型隐藏在子类中,同时面向客戶端的类仅仅只是对元素类型的泛型化类型。

带有 Self 的协议

协议内幕

func f<C: CustomStringConvertible>(_ x: C) -> Int {
  return MemoryLayout.size(ofValue: x)
}

func g(_ x: CustomStringConvertible) -> Int {
  return MemoryLayout.size(ofValue: x)
}

f(5) *// 8 *
g(5) *// 40 *

因为 f 接受的是泛型参数,整数 5 会被直接传递给这个函数,而不需要经过任何包装。所以它的大小是 8 字节,也就是 64 位系统中 Int 的尺寸。

对于 g,整数会被封装到一个存在容器中。对于普通的协议 (也就是没有被约束为只能由 class 实现的协议),会使用不透明存在容器 (opaque existential container)。

不透明存在容器中含有一个存储值的缓冲区 (大小为三个指针,也就是 24 字节);一些元数据 (一个指针,8 字节); 以及若干个目击表 (0 个或者多个指针,每个 8 字节)。

如果值无法放在缓冲区里,那么它将被存储到堆上,缓冲区里将变为存储引用,它将指向值在堆上的地址。元数据里包含关于类型的信息 (比如是否能够按条件进行类型转换等)。

目击表是让动态派发成为可能的关键。它为一个特定的类型将协议的实现进行编码: 对于协议中的每个方法,表中会包含一个指向特定类型中的实现的入口。有时候这被称为 vtable

typealias Any = protocol<> 

MemoryLayout<Any>.size // 32

protocol Prot { }
protocol Prot2 { }
protocol Prot3 { }
protocol Prot4 { }
typealias P = Prot & Prot2 & Prot3 & Prot4 

MemoryLayout<P>.size // 64

对于只适用于类的协议 (也就是带有 SomeProtocol: class 或者 @objc 声明的协议),会有一个叫做 类存在容器 的特殊存在容器,这个容器的尺寸只有两个字⻓ (以及每个额外的目击表增加一个字⻓),一个用来存储元数据,另一个 (而不像普通存在容器中的三个) 用来存储指向这个类的一个引用:

protocol ClassOnly: AnyObject {} 
MemoryLayout<ClassOnly>.size // 16 

性能影响

但是,如果你想要获取最大化的性能的时候,使用 泛型参数 确实要比使用 协议类型 高效得多。通过使用泛型参数,你可以避免隐式的泛型封装。

如果你尝试将一个 [String] (或者其他任何类型) 传递给一个接受 [Any] (或者其他任意接受协议类型,而非具体类型的数组) 的函数时,编译器将会插入代码对数组进行映射,将每个值都包装起来。

这将使方法调用本身成为一个 O(n) 的操作 (其中 n 是数组中的元素个数),这还不包含 函数体的复杂度。

// 隐式打包
func printProtocol(array: [CustomStringConvertible]) { 
  print(array) 
} 

// 没有打包
func printGeneric<A: CustomStringConvertible>(array: [A]) { 
  print(array) 
} 

接口和实现的耦合

使用协议最大的好处在于它们提供了一种最小的实现接口。

12. 互用性

实践: 封装 CommonMark

Swift 的依赖是基于模块的。

对于 C 或者 Objective-C 的库来说,想要它们在 Swift 编译器中可⻅,库必须按照 Clang 模块的格式提供一份模块地图 (module map)。模块地图中最重要的事情是列举出组成模块所使用的头文件。

封装 C 代码库

封装 cmark_node 类型

Swift 将 C 枚举导入为一个包含单个 UInt32 属性的结构体。除此之外,对原来枚举中的每个成员,Swift 还会为它生成一个顶层的变量

更安全的接口

低层级类型概览

// → 含有 managed 的类型代表内存是自动管理的。编译器将负责为你申请,初始化并且释放内存。

// → 含有 unsafe 的类型不提供自动的内存管理(这个 managed 正好相反)。你需要明确地进行内存申请,初始化,销毁和回收。

// → 含有 buffer 类型表示作用于一连串的多个元素,而非一个单独的元素上,它也提供了 Collection 的接口。

// → raw 类型包含无类型的原始数据,它和 C 的 void* 是等价的。在类型名字中不包含 raw 的类型的数据是具有类型的。

// → mutable 类型允许它指向的内存发生改变。

指针

UnsafePointer 是最基础的指针类型,它与 C 中的 const 指针类似。 UnsafePointer<Int> == const int*。

UnsafeMutablePointer

UnsafeMutableRawPointer 和 UnsafeRawPointer 类型 == void* 或者 const void*

assumingMemoryBound(to:)、bindMemory(to:) 或 load(fromByteOffset:as:)。

在底层,UnsafePointer<T> 和 Optional<UnsafePointer<T>> 的内存结构完全相同; 编译器会将 .none 的 case 映射为一个所有位全为零的 null 指针。

OpaquePointer - cmark_node 的定义并没有在头文件中暴露,所以,我们不能访问到指针指向的内存。

Unsafe[Mutable]BufferPointer - 然而,有时候你却不想为每个元素创建复制。
你通过一个指向起始元素的指针和元素个数的数字来初始化这个类型。

Unsafe[Mutable]RawBufferPointer - 类型让我们可以将那些原始数据当作集合来处理,这非常方便 (因为它们可以在底层提供与 Data 和 NSData 等价的类型)。

Tip:

虽然指针需要你手动进行内存的分配和释放,但是它们对于你通过指针存储的元素, 依然会执行标准的 ARC 内存管理操作。当你有一个 Pointee 类型是类的非安全可变指针时,它将会对你通过 initialize 存储在里面的每个对象进行 retain 操作,并且在当你调用 deinitialize 对它们做 release。

函数指针

<1> 在 Swift 中,MemoryLayout.size 返回的是一个类型的真实尺寸,但是对于那些在内存中的元素,平台的内存对⻬规则可能会导致相邻元素之间存在空隙。 stride 获取的是这个类型的尺寸,再加上空隙的宽度 (这个宽度可能为 0)。

<2> 当你将代码从 C 转换为 Swift 时,对于 C 中的 sizeof,在 Swift 中使用 MemoryLayout.stride 会更加合理。

<3> C 函数指针仅仅只是单纯的指针,它们不能捕获任何值。因为这个原因,编译器将只允许你提供不捕获任何局部变量的闭包作为最后一个参数。@convention(c) 这个参数属性就是用来保证这个前提的。

一般化

大多数和回调相关的 C API 都提供另外一种解决方式: 它们接受一个额外的不安全的 void 指针作为参数,并且在调用回调函数时将这个指针再传递回给调用者。

这样一来,API 的用戶可以在每次调用这个带有回调的函数时传递一小段随机数据进去,然后在回调中就可以判别调用者究竟是谁。

public func qsort_r(
  _ __base: UnsafeMutableRawPointer!,
  _ __nel: Int,
  _ __width: Int,
  _ __thunk: UnsafeMutableRawPointer!,
  _ __compar: @escaping @convention(c)
 (UnsafeMutableRawPointer?, UnsafeRawPointer?, UnsafeRawPointer?)
 -> Int32
)

typealias Block = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32

func qsort_block(_ array: UnsafeMutableRawPointer, _ count: Int, _ width: Int, f: @escaping Block)
{
 var thunk = f
 qsort_r(array, count, width, &thunk) { (ctx, p1, p2) -> Int32 in
    let comp = ctx!.assumingMemoryBound(to: Block.self).pointee
    return comp(p1, p2)
 }
}

extension Array where Element: Comparable {
  mutating func quicksort() {
    qsort_block(&self, self.count, MemoryLayout<Element>.stride) { a, b in
      let l = a!.assumingMemoryBound(to: Element.self).pointee
      let r = b!.assumingMemoryBound(to: Element.self).pointee
      if r > l { return -1 }
      else if r == l { return 0 }
      else { return 1 }
    }
  }
}

var x = [3,1,2] 
x.quicksort() 
x // [1, 2, 3]

不过,除了排序以外,还有很多有意思的 C API。而将它们以 类型安全泛型接口 的方式进行封装所用到的技巧,与我们上面的例子是一致的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容