第六章 Combine的组合操作符(prepend、append)

1,021 阅读4分钟

在本章中,您将了解一种更复杂但更有用的运算符类别:组合运算符。这组运算符允许您组合不同发布者发出的事件,并在您的组合代码中创建有意义的数据组合。 为什么组合有用?考虑一个包含多个用户输入的表单,比如:一个用户名、一个密码和一个复选框。您需要将这些不同的数据组合起来,以组成一个包含您需要的所有信息的发布者。

您将在这里慢慢开始使用一组操作符,这些操作符都是关于在您的发布者开头添加值的。换句话说,您将使用它们来添加在来自原始发布者的任何值之前发出的值。 在本节中,您将了解 prepend(Output...)、prepend(Sequence) 和 prepend(Publisher)。

prepend系列操作符

prepend(Output...)

这种 prepend 使用 ... 语法采用可变参数列表。这意味着它可以采用任意数量的值,只要它们与原始发布者的输出类型相同

img1210.jpg

在playground中加入如下代码:

  // 1
  let publisher = [3, 4].publisher
  
  // 2
  publisher
    .prepend(1, 2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布数字 3, 4 的发布者。
  2. 使用 prepend 在发布者自己的值之前添加数字 1 和 2。

运行playground,得到结果:

1
2
3
4

当然我们还可以继续在前面添加值,比如在playground的原prepend行之增加一行,修改后的源代码为:

// 1
let publisher = [3, 4].publisher

// 2
publisher
    .prepend(1, 2)
    .prepend(-1, 0)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

运行playground,得到结果:

-1
0
1
2
3
4

请注意,操作顺序在这里至关重要。最后一个前缀首先影响上游,这意味着 -1 和 0 在前面,然后是 1 和 2,最后是原始发布者的值

prepend(Sequence)

这种 prepend 与上一个类似,不同之处在于它将任何符合 Sequence 的对象作为输入。例如,它可能需要一个数组或一个集合。

img1220.jpg

在playground中加入如下代码:

  // 1
  let publisher = [5, 6, 7].publisher
  
  // 2
  publisher
    .prepend([3, 4])
    .prepend(Set(1...2))
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布数字 5、6 和 7 的发布者。
  2. Combine流程用 prepend(Sequence) 两次到原始发布者。一次从数组中添加值,第二次从集合中添加值

运行playground,得到结果:

1
2
3
4
5
6
7

与数组相反,关于集合要记住的一个重要事实是它们是无序的,因此不能保证项目发出的顺序。这意味着上例中的前两个值可以是 1 和 2,也可以是 2 和 1。

prepend(Publisher)

前两个操作符将值预先添加到现有发布者。但是,如果您有两个不同的出版商并且您想将他们的价值结合在一起怎么办?您可以使用 prepend(Publisher) 在原始发布者的值之前添加第二个发布者发出的值。

img1230.jpg

在playground中加入如下代码:

  // 1
  let publisher1 = [3, 4].publisher
  let publisher2 = [1, 2].publisher
  
  // 2
  publisher1
    .prepend(publisher2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建两个发布者。一个发布数字 3 和 4,第二个发布数字 1 和 2。
  2. 将publisher2 添加到publisher1 的开头。只有在发布者 2 发送 .finished 完成事件后,发布者 1 才会开始执行其工作并发出事件。

运行playground,得到结果:

1
2
3
4

正如预期的那样,值 1 和 2 首先从发布者 2 发出;只有这样,publisher1 才会发出 3 和 4。 这个例子中,对于发布者2发送.finished完成事件表现的并不明显,所以我们改成如下代码:

  // 1
  let publisher1 = [3, 4].publisher
  let publisher2 = PassthroughSubject<Int, Never>()
  
  // 2
  publisher1
    .prepend(publisher2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

  // 3
  publisher2.send(1)
  publisher2.send(2)
  // 4
  publisher2.send(completion: .finished)

  1. 创建两个发布者。第一个发出值 3 和 4,而第二个是可以动态发布值的 PassthroughSubject。
  2. 在publisher1 之前添加PassthroughSubject生成的发布者publisher2。
  3. 通过publisher2 发送值1 和2。
  4. 让publisher2发布.finished事件

我们用PassthroughSubject来手动发布数据,让prepend的功能展现的更加明显

运行playground,得到结果:

1
2
3
4

注意,如果省略掉第4步,也就是publisher.send(completion: .finished),则运行playground只会打印出(1 和 2),这是因为

  1. publisher1的prepend没有执行完毕
  2. pubisher2发布的数据可以被Combine流程接收到
  3. publisher1发布的数据只有在prepend执行完毕(收到publisher2的.finished后)才能被接收到

append系列操作符

这组运算符将发布者发出的事件与其他值连接起来。但在这种情况下,您将使用 append(Output...)、append(Sequence) 和 append(Publisher) 处理附加而不是前置。这些操作符的工作方式与它们的前置操作符类似

append(Output...)

append(Output...) 的工作方式类似于 prepend ,不过顺序相反,它是在后方向添加:它也接受一个类型为 Output 的可变参数列表,需要在原始发布者完成一个 .finished 事件后才可以添加到数据中

img1245.jpg

在playground中加入如下代码:

 // 1
 let publisher = [1].publisher

 // 2
 publisher
   .append(2, 3)
   .append(4) 
   .sink(receiveValue: { print($0) })
   .store(in: &subscriptions)
  1. 创建一个仅发出一个值的发布者:1。
  2. 使用 append 两次,首先是附加 2 和 3,然后是附加 4。

 运行playground,得到结果:

1
2
3
4

追加的工作方式与您期望的完全一样,每个追加都等待上游完成,然后再向其添加自己的工作。 这意味着上游必须完成否则append无法工作,因为Combine 无法知道前一个发布者已完成其所有值的发布。

为验证这一点,我们用另一个playground的代码来试验:

  // 1
  let publisher = PassthroughSubject<Int, Never>()

  publisher
    .append(3, 4)
    .append(5)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  // 2
  publisher.send(1)
  publisher.send(2)

经过前面几章,我们不过分解读这部分代码,我相信大家都看得懂。运行playground,得到结果:

1
2

这是因为我们用PassthroughSubject手动发布数据,没有发布.finished,所以publisher本身没有完成之前,两个append都无法工作 在上面代码最后添加让publisher结束的代码

publisher.send(completion: .finished)

再次运行playground,得到结果:

1
2
3
4
5

现在是我们想要的结果。 我们要重点记住的就是:prepend相比,对于整个append运算符系列,这种行为是相同的;除非前一个发布者发送一个 .finished 完成事件,否则不会发生追加。

append(Sequence)

这种append接受任何符合序列的对象,并在原始发布者的所有值发出后追加其值。

img1259.jpg

在playground中加入如下代码:

  // 1
  let publisher = [1, 2, 3].publisher
    
  publisher
    .append([4, 5]) // 2
    .append(Set([6, 7])) // 3
    .append(stride(from: 8, to: 11, by: 2)) // 4
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布 1、2 和 3 的发布者。
  2. 附加一个值为 4 和 5(有序)的数组。
  3. 附加一个值为 6 和 7(无序)的 Set。
  4. 附加一个以 2 为步长在 8 到 11 之间跨步的 Strideable

运行playground,得到结果:

1
2
3
4
5
7
6
8
10

如您所见,追加的执行是顺序的,因为前一个发布者必须在下一个追加执行之前完成。请注意,6 和 7 的集合对您来说可能具有不同的顺序,因为集合是无序的。

append(Publisher)

append操作符组的最后一个成员是用publisher作为参数。

img1265.jpg

在playground中加入如下代码:

  // 1
  let publisher1 = [1, 2].publisher
  let publisher2 = [3, 4].publisher
  
  // 2
  publisher1
    .append(publisher2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建两个发布者,第一个发布 1 和 2,第二个发布 3 和 4。
  2. 将publisher2 附加到publisher1,publisher2 的所有值都会附加到publisher1 的末尾

运行playground,得到结果:

1
2
3
4

关于append(Publisher)我需要再举一个例子来说明,我们看如下代码:

// 1
let publisher1 = PassthroughSubject<[Int], Never>()
let publisher2 = PassthroughSubject<[Int], Never>()

// 2
publisher1
    .append(publisher2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 3
publisher2.send([3,4])

//4 
publisher1.send([1,2])
publisher1.send(completion: .finished)

// 5
publisher2.send([5,6])
// 6
publisher2.send(completion: .finished)
publisher2.send([7,8])

这里我们不使用Sequence类型的Publisher,而使用可以手动发布数据和完成状态的PassthroughSubject

  1. 创建两个Subject类型的发布者,有PassthroughtSubject发布
  2. 在publisher1上附加publisher2
  3. publisher2手动发布一个数组 [3, 4]
  4. publisher1手动发布一个数组[1, 2],同时发布一个.finished状态
  5. publisher2再次手动发布一个数组[5, 6]
  6. publisher2手动发布.finished状态,然后发布一个数组[7, 8]

运行playground之前,您可以思考下最终打印结果。

让我们看下打印结果

[1, 2]
[5, 6]

这是因为如果要执行append(publisher2),必须需要首先publisher1本身是“完成状态”,所以第一次publisher2发布的[3, 4]不被处理。当publisher1发布了[1, 2]和.finished后,append(publisher2)开始被执行,此时publisher2发布的[5, 6]被附加到了publisher1的Output的末尾,成为了[1,2] , [5, 6],之后publisher2发布了.finished状态,而后publisher2发布的数据都无法被接收了。