第五章 Combine的过滤操作符(filter、removeDuplicates、compactMap、ignoreOutput、first、last)

1,437 阅读4分钟

正如您现在可能已经意识到的那样,操作符基本上就是您用来操纵 Combine 发布者的词汇。您知道的操作符越多,您对数据的控制就越好。 在前一章中,您学习了如何使用值并将它们转换为其他值——绝对是您日常工作中最有用的运算符类别之一。 但是当您想限制发布者发出的值或事件,并且只使用其中的一些时会发生什么?本章是关于如何使用一组特殊的运算符来做到这一点的:过滤运算符! 幸运的是,这些运算符中有许多在 Swift 标准库中具有相同的名称,因此如果您能够过滤本章的某些内容,请不要感到惊讶。

过滤基础

第一部分将涉及过滤的基础知识:有条件地决定将哪些值传递给用户。 最简单的方法是使用运算符——filter,它有一个返回 Bool 的闭包,返回为true的数据会被留下来,false的数据会被过滤掉

img1095.jpg

在playground中加入如下代码:

  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .filter { $0.isMultiple(of: 3) }
    .sink(receiveValue: { n in
      print("\(n) is a multiple of 3!")
    })
    .store(in: &subscriptions)

上述代码中,我们实现了:

  1. 创建一个新的发布者,它是Sequence类型的发布者,它将发出有限数量的值——1 到 10。
  2. 使用filter运算符,在闭包中进行逻辑判断,这里用的判断条件是:数据是否是3的倍数。

运行playground,得到结果

3 is a multiple of 3!
6 is a multiple of 3!
9 is a multiple of 3!

在你的应用程序的生命周期中,很多时候你会遇到发布者连续发布相同的值,而这些值你可能想要忽略。例如,如果用户连续键入“a”五次,然后键入“b”,您可能希望忽略过多的“a”。 Combine 为任务提供了完美的操作符:removeDuplicates

img1101.jpg

请注意,您不必为此运算符提供任何参数。 removeDuplicates 自动适用于符合 Equatable协议的任何值,包括 String类型

请看如下代码:

// 1
  let words = "hey hey there! want to listen to mister mister ?"
                  .components(separatedBy: " ")
                  .publisher
  // 2
  words
    .removeDuplicates()
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 将一个句子分成一组单词(例如,[String]),然后创建一个新的发布者来发布这些单词。
  2. 将 removeDuplicates() 应用于您的词发布者。

运行playground,得到结果:

hey
there!
want
to
listen
to
mister
?

可以看到,removeDuplicates过滤掉了重复的heymister

不符合 Equatable 的值怎么办?好吧,removeDuplicates 有另一个重载,它接受一个包含两个值的闭包,从中您将返回一个 Bool 来指示值是否相等

Compacting and ignoring(扁平化和忽略)

compactMap

很多时候,您会发现自己与发布 Optional 值的发布者打交道。或者更常见的是,您希望对可能返回 nil 的值执行一些操作,Swift标准库中有compactMap 可以完成这项工作,同时也有一个同名的Operators可以用在Combine中。 他的主要作用就是过滤掉nil值。

img1111.jpg

在playground中加入如下代码:

  // 1
  let strings = ["a", "1.24", "3",
                 "def", "45", "0.23"].publisher
  
  // 2
  strings
    .compactMap { Float($0) }
    .sink(receiveValue: {
      // 3
      print($0)
    })
    .store(in: &subscriptions)
  1. 创建一个字符串数组的发布者
  2. 使用 compactMap 尝试从每个单独的字符串初始化 Float。如果 Float 的初始化程序不知道如何转换提供的字符串,则返回 nil。这些 nil 值会被 compactMap 操作符自动过滤掉。
  3. 仅打印已成功转换为浮点数的字符串

运行playground,得到结果:

1.24
3.0
45.0
0.23

ignoreOutput

忽略发布者的Output

img1118.jpg

如上图所示,发出哪些值或发出多少个值并不重要,因为它们都被忽略了;您只需将完成事件推送给订阅者。

在playground中加入如下代码:

  // 1
  let numbers = (1...10_000).publisher
  
  // 2
  numbers
    .ignoreOutput()
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布从 1 到 10,000 的 10,000 个值的发布者。
  2. 添加 ignoreOutput 操作符,它忽略所有值并且只向消费者发出完成事件。

运行playground,你会发现打印信息为:

Completed with: finished

sink的receiveValue这里没有任何数据被接收到,因为ignoreOutput操作符忽略掉了所有发布的数据,只是将发布结束的状态通知了订阅者。

查询数据

first

这个运算符很有趣,因为它很懒惰,它在发布值中查找与条件匹配的值,一旦找到匹配项,它就会取消订阅并完成。

![[img1124.jpg]]

在playground中加入如下代码:

  // 1
  let numbers = (1...9).publisher
  
  // 2
  numbers
    .first(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个新的发布者,发出从 1 到 9 的数字。
  2. 使用 first(where:) 运算符来查找第一个发出的偶数值

运行playground,得到结果:

2
Completed with: finished

first操作符找到第一个符合条件的数据2之后,就完成了订阅。

现在我们通过加入print调试操作符来看下整个流程发生了什么。在numbers.first(where:)两行之间增加一行代码

 .print("numbers")

再次运行playground,得到结果:

numbers: receive subscription: (1...9)
numbers: request unlimited
numbers: receive value: (1)
numbers: receive value: (2)
`numbers: receive cancel`
2
Completed with: finished

如您所见,一旦 first(where:) 找到匹配的值,它就会通过订阅发送取消消息,从而导致上游停止发送值。

last

与 first(where:) 不同,这个操作符是贪婪的,因为它必须等待发布者完成发送值才能知道是否找到了匹配的值。因此,上游数据必须是有限的。

img1134.jpg 在playground中加入如下代码:

  // 1
  let numbers = (1...9).publisher
  
  // 2
  numbers
    .last(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  1. 创建一个发布 1 到 9 之间数字的发布者。
  2. 使用 last(where:) 运算符查找最后发出的偶数值

运行playground,得到结果:

8
Completed with: finished

现在我们将代码替换为如下代码,可以更清晰的理解为何称last贪婪

let numbers = PassthroughSubject<Int, Never>()
  
  numbers
    .last(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  numbers.send(1)
  numbers.send(2)
  numbers.send(3)
  numbers.send(4)
  numbers.send(5) 

这里我们使用PassthroughSubject这个符合Subject协议的Publisher,来通过 send 手动发布一些数据。

运行playground,你会发现没有任何打印信息。

正如预期的那样,由于发布者永远不会完成,因此无法确定与条件匹配的最后一个值。

现在我们在代码最后加上一行,来手动发布一个完成的信息

numbers.send(completion: .finished)

再次运行playground,得到结果:

4
Completed with: finishedb