第四章 Combine的转换操作符(replaceNil、replaceEmpty、scan)

1,115 阅读4分钟

replaceNil(with:)

如下面的弹珠图所示,replaceNil 将接收可选值并将 nil 替换为您指定的值

img1048.jpg

我们可以在playground中输入如下代码来测试他的功能:

["A", nil, "C"].publisher	// 1
    .eraseToAnyPublisher()
    .replaceNil(with: "-") // 2
    .sink(receiveValue: { print($0) }) // 3
    .store(in: &subscriptions)
  1. 从可选字符串数组创建Publisher。
  2. 使用 replaceNil(with:) 将从上游发布者收到的 nil 值替换为"-"
  3. 打印

打印结果如下

A
-
C

使用“??”运算符和replaceNil有个重要的区别:

“??”运算符可以得到一个nil的结果,但是replaceNil不会

replaceEmpty(with:)

你可以使用 replaceEmpty(with:) 操作符来替换一个值(如果发布者完成了但没有发出一个值)。 在下面的弹珠图中,发布者在不发出任何内容的情况下完成,此时 replaceEmpty(with:) 操作符插入一个值并将其发布到下游

img1054.jpg

在playground中输入以下代码:

// 1
  let empty = Empty<Int, Never>()
  
  // 2
  empty
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个立即发出完成事件的空发布者。
  2. 订阅它,并打印收到的事件

使用 Empty 发布者类型来创建一个立即发出 .finished 完成事件的发布者。您还可以通过将 false 传递给其 completeImmediately 参数(默认情况下为 true)将其配置为从不发出任何内容。

Empty对于演示或测试目的非常有用,或者当您想要做的只是向订阅者发出某些任务完成的信号时。

运行playground,你会看到它成功

finished

现在,在调用 sink 之前插入这行代码

.replaceEmpty(with: 1)

再次运行playground,会看到如下打印信息

1
finished

增量转换数据

你已经看到了 Combine 如何包含诸如 map 之类的运算符,这些运算符与 Swift 标准库中的高阶函数对应且工作方式类似。但是,Combine 还有一些其他技巧,可以让您操纵从上游发布者收到的值。

scan(::)

转换类别中的一个很好的例子是扫描。它会将上游发布者发出的当前值提供给闭包,以及该闭包返回的最后一个值。 在下面的弹珠图中,扫描从存储一个起始值 0 开始。当它从发布者那里接收到每个值时,它将它添加到之前存储的值上,然后存储并发出结果

img1064.jpg

书中原Demo示例稍微复杂,我这里呈现一个更简单的Demo,可以更直观的介绍scan的用法

在playground中输入下列代码:

_ = [3, 4, 5].publisher		// 1
	// 2
    .scan(2) { initValue, indexValue -> Int in
        print("initValue = \(initValue), indexValue = \(indexValue)")
        return initValue * indexValue
    }
    .sink(receiveCompletion: { completion in
        print("completion: \(completion)")
    }, receiveValue: { value in
		// 3
        print("receiveValue: \(value)")
    })
  1. 从Int数组创建一个Publisher。
  2. 我们这里希望用scan计算数组中每个元素的相乘,同时乘以初始值的结果(也就是(3*4*5) * 2的结果)
  3. 打印出每次sink收到的结果

运行playground,看到如下打印信息:

initValue = 2, indexValue = 3
initValue = 6, indexValue = 4
initValue = 24, indexValue = 5
receiveValue: 6
receiveValue: 24
receiveValue: 120
completion: finished

希望您能先观察打印结果,同时讲解scan的参数用法,这样可以更直观。

scan初始化的函数是

public func scan<T>(_ initialResult: T, _ nextPartialResult: @escaping (T, Publishers.Sequence<Elements, Failure>.Output) -> T) -> Publishers.Sequence<[T], Failure>

可以看到它需要3个参数,一个是设置的初始值,一个是下一个值,也就是要被遍历到的下一个值,最后是一个@escaping 闭包,来实现对数据的处理。

然后我们再回头看下playground的输出信息:

  • 前三行,表示scan运行了3次,因为Publisher的Output是一个包含三个数据的的数组。
  • scan每次都是进行 初始值 * 下一个Output数据,然后其结果会被存储到初始值
  • 循环3次结束后,scan部分完成,开始进入sink阶段
  • sink里,我们也不是一次性接收到最终的结果(120),而是也是分成了3次接收。因为scan操作符返回的Publisher类型,是Publishers.Sequence<[T], Failure>

所以虽然我们的demo是计算数组各个元素相乘的结果,但实际上要计算这个结果,用scan不是最好的选择

本章的Key Points

  • 您调用对发布者的输出执行操作的方法为“操作符”。
  • 操作符(Operators)也是发布者(Publisher)。
  • 转换运算符将来自上游发布者的输入转换为适合下游使用的输出。
  • 弹珠图是可视化每个组合运算符如何工作的好方法。
  • 在使用任何缓冲值的运算符(例如 collect 或 flatMap )时要小心,以避免内存问题。
  • 在应用 Swift 标准库中已有的函数知识时要注意。一些名称相似的组合运算符的工作方式相同,而另一些则完全不同。
  • 在订阅中将多个运算符链接在一起以对发布者发出的事件创建复杂的复合转换是很常见的