第五章 Combine的过滤操作符(drop系列操作符、prefix系列操作符)

859 阅读3分钟

删除值

删除值是您在使用Combine进行数据处理时经常需要利用的有用功能。例如,当您想在第二个发布者开始发布之前忽略来自一个发布者的值,或者如果您想在Combine流开始时忽略特定数量的值,您可以使用它。 三个运算符属于这一类,您将首先学习最简单的一个 - dropFirst。

dropFirst

dropFirst 运算符采用计数参数——如果省略则默认为 1——并忽略发布者发出的第一个计数值。只有在跳过计数值之后,发布者才会开始传递值。

img1144.jpg

在playground加入如下代码:

  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .dropFirst(8)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布 1 到 10 之间的 10 个数字的发布者。
  2. 使用 dropFirst(8) 删除前八个值,只打印 9 和 10。

运行playground,得到结果:

9
10

drop(while:)

这是另一个非常有用的变体,它采用闭包并忽略发布者发出的任何值,直到第一次满足闭包中的条件。

img1150.jpg

在playground中加入如下代码:

  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .drop(while: { $0 % 5 != 0 })
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布 1 到 10 之间数字的发布者。
  2. 使用 drop(while:) 等待第一个可被 5 整除的值。当闭包中的条件为false,将此数据发到下游,否则丢掉这个数据。

运行playground,得到结果:

5
6
7
8
9
10

回顾之前我们讨论过的filter,你会发现他们之间很相似,但也有明显不同:

  • 如果您在闭包中返回 true,则 filter 允许值通过,而 drop(while:) 只要您从闭包中返回 true会丢掉该值。
  • filter永远不会停止评估上游发布者发布的所有值的条件。即使在过滤器的条件评估为真之后,进一步的值仍然“受到质疑”,你的闭包必须回答这个问题:你想让这个值通过吗?相反,drop(while:) 的闭包在条件满足后将永远不会再次执行

    上段是直译出来的文字,用更好理解的话来讲,就是: filter会一直工作,一直去判断上游的数据是否符合filter闭包的判断条件。而drop(while:)则是在第一次判断闭包的判断结果为true时,就停止工作,不再判断上游数据。

对于第二点,我们将代码中的

.drop(while: { $0 % 5 != 0 })

替换为

.drop(while: {
  print("x")
  return $0 % 5 != 0
})

再次运行playground,得到结果为:

`x`
`x`
`x`
`x`
`x`
5
6
7
8
9
10

您可以看出,x 正好打印了五次。一旦满足条件(当发出 5 时),闭包就不再执行。

drop(untilOutputFrom:)

跳过发布者发出的任何值,直到第二个发布者开始发出值,从而在它们之间建立关系。

想象一个场景,你有一个用户点击一个按钮,但你想忽略所有点击,直到你的 isReady 发布者发出一些结果。这个运算符非常适合这种情况。

img1161.jpg

第一行代表 isReady 流,第二行代表用户通过 drop(untilOutputFrom:) 进行的点击,它以 isReady 作为参数。

在playground中加入如下代码:

  // 1
  let isReady = PassthroughSubject<Void, Never>()
  let taps = PassthroughSubject<Int, Never>()
  
  // 2
  taps
    .drop(untilOutputFrom: isReady)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  // 3
  (1...5).forEach { n in
    taps.send(n)
    
    if n == 3 {
      isReady.send()
    }
  }
  1. 创建两个可以手动发送值的 PassthroughSubjects。第一个是 isReady 而第二个代表用户的点击。
  2. 使用 drop(untilOutputFrom: isReady) 忽略用户的任何点击,直到 isReady 发出至少一个值。
  3. 发布5次“点击”,就像上图一样。第三次点击后,你发送 isReady 一个值

运行playground,得到结果:

4
5

代码的流程为:

  • 用户有五次点击。前三个被忽略。
  • 第三次点击后, isReady 会发出一个值。
  • 用户以后的所有点击都会通过

这个代码中需要特别注意的是: 如果在isReady.send()之后不再发送taps.send,则不会进入sink流程。

极限值(Limiting values)

本节解决了相反的需求:接收值直到满足某些条件,然后强制发布者完成。例如,考虑一个可能会发出未知数量值的请求,但您只需要一次发出而不关心其余的值。

Combine 使用一系列prefix运算符解决了这组问题。尽管名称并不完全直观,但这些运算符提供的功能在许多现实生活中都很有用。

prefixdrop类似,并提供prefix(_:)prefix(while:)prefix(untilOutputFrom:)。不同的是,prefix不会在满足某个条件之前删除值,而是在满足该条件之前取值。

prefix

作为 dropFirst 的反面,prefix(_:) 只会取值到提供的数量然后完成。

img1171.jpg

在playground加入如下代码:

  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .prefix(2)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布 1 到 10 数字的发布者。
  2. 使用 prefix(2) 只允许发布前两个值。一旦发出两个值,就会在sink收到competion消息

运行playground,得到结果:

1
2
Completed with: finished

就像 first(where:) 一样,这个操作符是惰性的,这意味着它只占用它需要的尽可能多的值,然后终止。这也可以防止数字产生超过 1 和 2 的其他值,因为它也会完成。

prefix(while:)

它接受一个闭包,如果闭包结果为true,则让来自上游发布者的值通过;如果结果为false,操作符将不再工作。

img1177.jpg

在playground中加入如下代码:

  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .prefix(while: { $0 < 3 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions) 
  1. 创建一个发布 1 到 10 之间的值的发布者。
  2. 使用 prefix(while:) 让小于 3 的值通过。一旦发出等于或大于 3 的值,发布就结束了。

运行playground,得到结果:

1
2
Completed with: finished

注意 prefix(while:)是当闭包中的判断结果为false时,结束操作符使命,操作符不再工作, 而 drop(while:)是当闭包中的判断结果为true时结束。

prefix(untilOutputFrom:)

前面讨论过的drop(untilOutputFrom:) 跳过值直到第二个发布者发布值 而prefix(untilOutputFrom:) 是取值直到第二个发布者发布值。

想象一个场景,你有一个按钮,用户只能点击两次。一旦发生两次点击,按钮上的更多点击事件应该被省略。

img1187.jpg

在playground中加入如下代码:

  // 1
  let isReady = PassthroughSubject<Void, Never>()
  let taps = PassthroughSubject<Int, Never>()
  
  // 2
  taps
    .prefix(untilOutputFrom: isReady)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  // 3
  (1...5).forEach { n in
    taps.send(n)
    
    if n == 2 {
      isReady.send()
    }
  }
  1. 创建两个可以手动发布值的 PassthroughSubjects。第一个是 isReady状态,第二个代表用户的点击事件。
  2. 使用 prefix(untilOutputFrom: isReady) 操作符处理taps这个publisher,它会等待直到 isReady 发出至少一个值。
  3. 通过主题发送五个“点击”,完全如上图所示。在第二次点击后,你用 isReady 发布了一个值。

运行playground,得到结果:

1
2
Completed with: finished

drop(untilOutputFrom:)prefix(untilOutputFrom:)有些难以理解,我们再次比较他们来加深印象

  • drop(untilOutputFrom: somePublisher)会在 somePublisher发布值之间,丢掉所有的上游数据。
  • prefix(untilOutputFrom: somePublisher)则是在somePublisher发布值之前,获得所有的上游数据。

书中最后是一个挑战的测试 创建一个发布从 1 到 100 的数字集合的示例,并使用过滤运算符来:

  1. 跳过上游发布者发出的前 50 个值。
  2. 在前 50 个值之后取接下来的 20 个值。
  3. 只取偶数

答案如下:

let numbers = (1...100).publisher
  numbers
    .dropFirst(50)
    .prefix(20)
    .filter { $0 % 2 == 0 }
    .sink(receiveValue: { print($0) })

本章的KeyPoints

  • 过滤操作符让你可以控制上游发布者发出的哪些值被发送到下游
  • 当你不关心值本身,只想要一个完成事件时,ignoreOutput 是一个好选择。
  • 查找是另一种过滤方式,您可以分别使用 first(where:) 和 last(where:) 找到闭包判断条件的第一个或最后一个值。
  • first系列操作符是懒惰的;它们只根据需要取尽可能多的值,然后完成。 last系列操作符是贪婪的,在决定哪个值最后满足条件之前必须知道值的全部范围。
  • 您可以使用 drop 系列操作符控制在向下游发送值之前忽略上游发布者发出的值的数量。
  • 同样,您可以使用prefix系列运算符控制上游发布者在完成之前可以发出多少个值。