Units

这个文档将尝试描述 units 是什么,为什么他们是有用的理念,并且如果使用和创建他们。

Why

Swift 拥有一个强大的类型系统,这能用来提高正确性和应用的稳定性,并且使使用 Rx 成为更直观和直接的经历。

Units 仅在 RxCocoa 项目中。然而如果有需要,同样的原理可以很轻松的被实现在其他 Rx 的实现中。这里没有私有API的魔法需要

Units 是完全可选的。你可以在你程序的任何地方使用原始的 observable 序列,并且所有 RxCocoa 的接口能和 observable 序列兼容使用

Units 还帮助交流和确认 observable 序列属性穿越接口边界。

当我们在写 Cocoa/UIKit 时,这有一些重要的属性。

  • 不能错误退出
  • 在主调度器上观察
  • 在主调度器上订阅
  • 分享副作用

How they work

在它的核心中,它只是一个引用 observable 序列的结构。

你可以把他们想成一种 observable 序列的构造模型。当一个序列被构造,调用 .asObservable() 将转换一个 unit 成为一个 observable 序列。

Why they are named Units

使用一对类比将会帮助我们解释不常见的理念。下面是一些类比告诉我们 units 在物理世界和 RxCocoa(Rx units)之间的相似点。

Analogies:

物理 units Rx units
数字 (一个值) observable 序列 (序列的值)
空间单位 (m, s, m/s, N …) Swift 结构体 (Driver, ControlProperty, ControlEvent, Variable, …)

一个物理单位是一对数字和其匹配的空间单位。

一个Rx单位(unit)是一对一个 observable 序列和其匹配的描述 observable 序列属性的结构体。

当使用物理单位时,数字是一个基本的组成胶水:比如实数和复数。

当我们使用 Rx 单位时,Observable 序列是一个基本的组成胶水。

物理单位和 dimensional analysis 可以在复杂计算中减缓一些错误级别。

数字有一些操作符: +, -, *, /.

Observable 序列也有一些操作符: map, filter, flatMap

物理单位通过使用相应的数字操作定义操作符,比如:

/ 操作在物理单位被定义为使用 / 操作数字。

11 m / 0.5 s = …

  • 首先,转换到数字单位,并且应用 / 操作符 11 / 0.5 = 22
  • 然后,计算单位(m / s)
  • 最后,组合结果 = 22 m / s

Rx 单位通过使用一致的 observable 序列操作符定义操作(这就是内部操作符如何运行的),例如

Driver 上的 map 操作符被定义为在其 observable 序列上使用 map 操作符。

  1. let d: Driver<Int> = Drive.just(11)
  2. driver.map { $0 / 0.5 } = ...
  • 首先,转换 Driverobservable 序列 并且 使用 map 操作符

    1. let mapped = driver.asObservable().map { $0 / 0.5 } // this `map` is defined on observable sequence
  • 然后,组合结果来获得单位值

    1. let result = Driver(mapped)

在物理中有一系列基本单位(m, kg, s, A, K, cd, mol)是正交的。

RxCocoa 中有一系列有趣的 observable 序列基础属性是正交的。

  1. * 不能错误退出
  2. * 在主调度器上观察
  3. * 订阅在主调度器上
  4. * 分享副作用

物理中派生的单位有时有专有的名称。

例如:

  1. N (牛顿) = kg * m / s / s
  2. C (库仑) = A * s
  3. T (特斯拉) = kg / A / s / s

Rx 派生的单位也有专有的名称。

例如:

  1. Driver = (不能错误退出) * (在主调度器上观察) * (分享副作用)
  2. ControlProperty = (分享副作用) * (订阅在主调度器上)
  3. Variable = (不能错误退出) * (分享副作用)

物理中转换不同的单位通过在数学上的操作符定义的 *, /

Rx 中转换不同的单位通过 observable 序列操作符的帮助。

例如:

  1. 不能错误退出 = catchError
  2. 在主调度器上观察 = observeOn(MainScheduler.instance)
  3. 订阅在主调度器上 = subscribeOn(MainScheduler.instance)
  4. 分享副作用 = share* (one of the `share` operators)

RxCocoa units

Driver unit 驱动单元

  • 不能错误退出
  • 在主调度器上观察
  • 分享副作用 (shareReplayLatestWhileConnected)

ControlProperty / ControlEvent 控制属性 / 控制事件

  • 不能错误退出
  • 在主调度器上订阅
  • 在主调度器上观察
  • 分享副作用

变量 (Variable)

  • 不能错误退出
  • 分享副作用

驱动 (Driver)

这是非常精心计划的单元。它的目的是提供一个直接的方式去写 UI 层的响应式代码。

为什么被命名为驱动(Driver)

它的目标使用场景是模型化驱动你应用的序列。

例如:

  • 从 CoreData 模型驱动 UI
  • 使用从其他 UI 元素的值驱动 UI (bindings)

类似普通的操作系统驱动,假如一个序列错误退出, 你的应用将停止用户输入的相应。

那些被观察在主线程的元素还非常的重要因为 UI 元素和应用逻辑通常是线程不安全的。

另外, 驱动(Drive) 单元构建了一个 observable 分享副作用的序列。

E.g.

Practical usage example 实际用例

这是一个典型的开始例子。

  1. let results = query.rx_text
  2. .throttle(0.3, scheduler: MainScheduler.instance)
  3. .flatMapLatest { query in
  4. fetchAutoCompleteItems(query)
  5. }
  6. results
  7. .map { "\($0.count)" }
  8. .bindTo(resultCount.rx_text)
  9. .addDisposableTo(disposeBag)
  10. results
  11. .bindTo(resultsTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
  12. cell.textLabel?.text = "\(result)"
  13. }
  14. .addDisposableTo(disposeBag)

这个代码的意图是:

  • Throttle 用户输入
  • 联系服务器并且获取一个用户列表(每一个请求)
  • 绑定结果到两个 UI 元素:显示结果到 tableView 和显示有多少条记录的标签。

那么,这个代码有什么问题?

  • 如果 fetchAutoCompleteItems observable 序列错误退出(连接失败或者处理错误),这个错误将会解绑一切并且 UI 不会响应更多的请求。
  • 如果 fetchAutoCompleteItems 在一些后台线程返回结果,结果会从后台线程被绑定到 UI 元素,而这会引起不确定的奔溃。
  • 结果被绑定到两个 UI 元素,这意味着每个用户查询会制造两个HTTP请求对应两个 UI 元素,这并不是预期的行为。

更合适的版本的代码应该看起来像如下:

  1. let results = query.rx_text
  2. .throttle(0.3, scheduler: MainScheduler.instance)
  3. .flatMapLatest { query in
  4. fetchAutoCompleteItems(query)
  5. .observeOn(MainScheduler.instance) // results are returned on MainScheduler
  6. .catchErrorJustReturn([]) // in the worst case, errors are handled
  7. }
  8. .shareReplay(1) // HTTP requests are shared and results replayed
  9. // to all UI elements
  10. results
  11. .map { "\($0.count)" }
  12. .bindTo(resultCount.rx_text)
  13. .addDisposableTo(disposeBag)
  14. results
  15. .bindTo(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
  16. cell.textLabel?.text = "\(result)"
  17. }
  18. .addDisposableTo(disposeBag)

在大型系统中确认所有这些要求被合适的处理是有挑战的,但是使用编译器和单位去证明这些要求被满足会有更简单方法。

下面代码看起来大致一样:

  1. let results = query.rx_text.asDriver() // This converts a normal sequence into a `Driver` sequence.
  2. .throttle(0.3, scheduler: MainScheduler.instance)
  3. .flatMapLatest { query in
  4. fetchAutoCompleteItems(query)
  5. .asDriver(onErrorJustReturn: []) // Builder just needs info about what to return in case of error.
  6. }
  7. results
  8. .map { "\($0.count)" }
  9. .drive(resultCount.rx_text) // If there is a `drive` method available instead of `bindTo`,
  10. .addDisposableTo(disposeBag) // that means that the compiler has proven that all properties
  11. // are satisfied.
  12. results
  13. .drive(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
  14. cell.textLabel?.text = "\(result)"
  15. }
  16. .addDisposableTo(disposeBag)

所以这里发生了什么?

第一个 asDriver 方法转换 ControlProperty 单元成为一个 Driver 单元。

  1. query.rx_text.asDriver()

注意这不需要任何其他的操作。驱动(Driver)有所有 ControlProperty 单元的属性,并且添加了其他。下面的 observable 序列只是被包装了作为 驱动Driver 单元。

第二个改变是:

  1. .asDriver(onErrorJustReturn: [])

任何 observable 序列都能被转换成 Driver驱动 单元,只要它满足3个属性:

  • 不能错误退出
  • 在主调度器上观察
  • 分享副作用(shareReplayLatestWhileConnected)

所以你如何确认这些属性被满足?仅使用 Rx 操作。asDriver(onErrorJustReturn: []) 等价于如下代码:

  1. let safeSequence = xs
  2. .observeOn(MainScheduler.instance) // observe events on main scheduler
  3. .catchErrorJustReturn(onErrorJustReturn) // can't error out
  4. .shareReplayLatestWhileConnected // side effects sharing
  5. return Driver(raw: safeSequence) // wrap it up

最终的片段是使用 drive 代替使用 bindTo

drive 仅在 Driver驱动 上被定义。这意味着如果你看见 drive 出现在代码中,observable 序列从不错误退出并且它在主线程上观察,这样绑定一个 UI 元素是安全的。

然而,理论上,有人可以定义一个 drive 方法工作在 ObservableType 上或者另一些接口,所以为了额外的安全,在绑定 UI 元素之前会用 let results: Driver<[Results]> = ... 创建一个临时的定义,为了完全验证这是必要的。然而,我们将留给读者去决定这是否是一个现实的情况。