跳转至

第4章:过滤操作符

正如你现在可能已经意识到的,操作符基本上是你用来操作Combine发布者的词汇。你知道的"词汇"越多,你对数据的控制就越好。

在上一章中,你学会了如何消耗数值并将其转化为其他数值--这绝对是对你日常工作最有用的操作符类别之一。

但是,当你想限制发布者发出的值或事件,只订阅其中的一部分时,会发生什么?本章就是关于如何用一组特殊的操作符来实现这一目的。筛选操作符!

幸运的是,这些操作符中有许多与Swift标准库中的名称相似,所以如果你能过滤本章的一些内容,不要感到惊讶。]

现在是时候直接进入了。

开始

你可以在projects文件夹中找到本章的启动Playground,即Starter.playground。随着本章的深入,你将在Playground上编写代码,然后运行Playground。这将帮助你理解不同的操作符是如何处理发布者发出的事件的。

Note

本章中的大多数操作符都有类似的try前缀,例如,filtertryFilter。它们之间唯一的区别是,后者提供了一个抛出闭包。你从闭包中抛出的任何错误都会使发布者与被抛出的错误一起终止。为了简洁起见,本章将只介绍不抛出的变化,因为它们几乎是相同的。

过滤的基础知识

第一节将讨论过滤的基础知识--订阅一个值的发布者,并有条件地决定将其中哪些值传递给订阅者。

最简单的方法是使用命名恰当的操作符--filter,它接收一个期望返回Bool的闭包。它将只传递符合所提供的谓词的值:

img

把这个新的例子添加到你的Playground

example(of: "filter") {
  // 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. 创建一个新的发布者,它将发布一个有限数量的值--110,然后完成,使用Sequence类型上的publisher属性。
  2. 使用filter操作符,传入一个谓词,只允许通过3的倍数的数字。

运行你的Playground。你应该在控制台看到以下内容:

——— Example of: filter ———
3 is a multiple of 3!
6 is a multiple of 3!
9 is a multiple of 3!

这样一个优雅的方式来骗取你的数学作业,不是吗?:]

在你的应用程序的生命周期中,很多时候你会有一些发布者在一排中发出相同的值,你可能想忽略这些值。例如,如果一个用户连续输入了五次"a",然后又输入了"b",你可能想忽略掉这些过多的"a"

Combine为这个任务提供了一个完美的操作符:removeDuplicates

img

注意你不需要为这个操作符提供任何参数。removeDuplicates自动对任何符合Equatable的值起作用,包括String

把下面这个removeDuplicates()的例子添加到你的Playground上 - 确保在words变量中的?前加入一个空格:

example(of: "removeDuplicates") {
  // 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()应用于你的words发布者。

运行你的Playground,看一下调试控制台:

——— Example of: removeDuplicates ———
hey
there!
want
to
listen
to
mister
?

正如你所看到的,你已经跳过了第二个"hey"和第二个"mister"。棒极了!

Note

那些不符合Equatable的值怎么办?嗯,removeDuplicates有另一个重载,它接收一个带有两个值的闭包,从中返回一个Bool来表示这些值是否相等。

压缩和忽略

很多时候,你会发现自己在处理一个发布者发出的Optional值。或者更常见的是,你想对你的值进行一些可能返回nil的操作,但谁想处理所有这些nil

如果你的嗅觉很灵敏,想到Swift标准库中一个非常著名的Sequence方法compactMap可以做这个工作,那么好消息是--还有一个同名的操作符!

img

在你的Playground上添加以下内容:

example(of: "compactMap") {
  // 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. 只打印已经成功转换为Float的字符串。

在你的Playground上运行上述例子,你应该看到类似上图的输出。

——— Example of: compactMap ———
1.24
3.0
45.0
0.23

好吧,你为什么不从所有这些数值中抽身出来呢......谁会关心这些,对吗?有时候,你只想知道发布者已经完成了数值的发射,而不考虑实际数值。当这种情况发生时,你可以使用ignoreOutput操作符:

img

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

通过在你的Playground上添加以下代码来实验这个例子:

example(of: "ignoreOutput") {
  // 1
  let numbers = (1...10_000).publisher

  // 2
  numbers
    .ignoreOutput()
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

在上述例子中,你:

  1. 创建一个发布者,从110,000发射10,000个值。
  2. 添加ignoreOutput操作符,省略所有的值,只向订阅者发送完成事件。

你能猜到这段代码的输出会是什么吗?

如果你猜到不会有任何值被打印出来,你是对的! 运行你的Playground,看看调试控制台:

——— Example of: ignoreOutput ———
Completed with: finished

寻找数值

在这一节中,你将了解到两个同样起源于Swift标准库的操作符。first(where:)last(where:)。正如它们的名字所暗示的那样,你可以用它们来分别寻找和发射与所提供的谓词相匹配的第一个或最后一个值。

现在来看看几个例子,从first(where:)开始。

img

这个操作符很有趣,因为它是懒惰的,也就是说。它只取它需要的值,直到它找到一个与你提供的谓词相匹配的值。一旦它找到一个匹配的,它就取消订阅并完成。

把下面这段代码添加到你的Playground,看看它是如何工作的:

example(of: "first(where:)") {
  // 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. 创建一个新的发布者,发射从19的数字。
  2. 使用first(where:)操作符来找到第一个发出的偶数值。

在你的Playground上运行这个例子,看看控制台的输出:

——— Example of: first(where:) ———
2
Completed with: finished

它的工作原理与你可能猜到的完全一样。但是,等等,对上游的订阅,也就是numbers发布者的订阅呢?它是否在找到一个匹配的偶数后仍继续发射其值?通过找到以下一行来测试这个理论:

numbers

然后在该行后面立即添加print("numbers")操作符,因此看起来如下:

numbers
  .print("numbers")

Note

你可以在操作符链的任何地方使用print操作符,以查看在该点发生的确切事件。

再次运行你的Playground,看一下控制台。你的输出应该与下面类似:

——— Example of: first(where:) ———
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(where:),它的目的是找到与所提供的谓词相匹配的最后一个值。

img

first(where:)相反,这个操作符是贪婪的,因为它必须等待发布者完成发射值,才能知道是否找到了匹配值。由于这个原因,上游必须是有限的。

将下面的代码添加到你的Playground中:

example(of: "last(where:)") {
  // 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. 创建一个发射19之间数字的发布者。
  2. 使用last(where:)操作符,找到最后发出的偶数值。

你猜到输出的结果是什么了吗?运行你的Playground并找出答案:

——— Example of: last(where:) ———
8
Completed with: finished

还记得我之前说过,发布者必须完成这个操作员的工作吗?这是为什么呢?

嗯,那是因为操作者没有办法知道发布者是否会在下行时发出一个符合标准的值,所以操作者必须知道发布者的全部范围,然后才能确定最后一个符合谓词的项目。

要看到这个实际情况,请将整个例子替换为以下内容:

example(of: "last(where:)") {
  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并通过它手动发送事件。

再次运行你的Playground,你应该看到......完全没有:

——— Example of: last(where:) ———

正如预期的那样,由于发布者从未完成,所以没有办法确定符合标准的最后一个值。

为了解决这个问题,在例子的最后一行添加以下内容,通过主题发送一个完成:

numbers.send(completion: .finished)

再次运行你的Playground,现在一切都应按预期工作:

——— Example of: last(where:) ———
4
Completed with: finished

我想,一切都必须有一个结束......或完成,在这种情况下。

Note

你也可以使用first()last()操作符来简单地获得发布者曾经发出的第一个或最后一个值。这些也是懒惰的和贪婪的,相应的。

丢弃值

当你与发布者一起工作时,丢弃值是一个有用的功能,你经常需要利用它。例如,当你想忽略一个发布者的值,直到第二个发布者开始发布时,或者当你想在数据流开始时忽略特定数量的值时,你可以使用它。

有三个操作符属于这个类别,你将首先学习最简单的一个--dropFirst

dropFirst操作符需要一个count参数--如果省略则默认为1--并忽略由发布者发出的第一个count值。只有在跳过count值之后,发布者才会开始传递值。

img

将以下代码添加到你的Playground的末尾,以尝试这个操作符:

example(of: "dropFirst") {
  // 1
  let numbers = (1...10).publisher

  // 2
  numbers
    .dropFirst(8)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
}

如同前面的图,你:

  1. 创建一个发布者,发布110之间的10个数字。
  2. 使用dropFirst(8)放弃前8个值,只打印910

运行你的Playground,你应该看到以下输出:

——— Example of: dropFirst ———
9
10

很简单,对吗?通常,最有用的操作符是!

接下来是丢弃值家族中的下一个操作符--drop(while:)。这是另一个极其有用的变体,它接受一个谓词闭包,忽略发布者发出的任何值,直到第一次满足该谓词。一旦不满足谓词,值就开始流经操作符:

img

将下面的例子添加到你的Playground上,看看这个动作:

example(of: "drop(while:)") {
  // 1
  let numbers = (1...10).publisher

  // 2
  numbers
    .drop(while: { $0 % 5 != 0 })
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
}

在下面的代码中,你:

  1. 创建一个发射110之间数字的发布者。
  2. 使用drop(while:)来等待第一个能被5整除的值。一旦条件不满足,数值将开始流经运算器,不再被丢弃。

运行你的Playground,看看调试控制台:

——— Example of: drop(while:) ———
5
6
7
8
9
10

非常好! 正如你所看到的,你已经放弃了前四个值。当5到达时,"这是否能被5整除?"这个问题最终成为true,所以它现在发出了5和所有未来的值。

你可能会问自己--这个操作符和filter有什么不同?它们都接受一个闭包,根据该闭包的结果来控制哪些值被发射出来。

第一个区别是,如果你在闭包中返回true,那么filter允许数值通过,而drop(while:)只要你在闭包中返回true,就会跳过数值。

第二个,也是更重要的区别是,filter对上游发布者发布的所有值都不会停止评估其条件。即使在filter的条件评估为true之后,更多的值仍然被"质疑",你的闭包必须回答这个问题。"你想让这个值通过吗?"。

相反,drop(while:)的谓词闭包在条件不满足后将不会再被执行。为了确认这一点,请替换下面这一行:

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

有了这段代码:

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

你添加了一个print语句,在每次调用闭包时将x打印到调试控制台。运行playground,你应该看到以下输出:

——— Example of: drop(while:) ———
x
x
x
x
x
5
6
7
8
9
10

你可能已经注意到了,x正好打印了五次。一旦条件不满足(当5被打印出来时),闭包就不会再被评估。

那好吧。两个操作符结束了,还有一个要做。

过滤类别中最后一个也是最复杂的操作符是drop(untilOutputFrom:)

想象一下这样的场景:用户点击了一个按钮,但你想忽略所有的点击,直到你的isReady发布者发出一些结果。这个操作符是这种情况的完美选择。

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

img

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

在你的Playground的末尾,添加以下代码来再现这个图:

example(of: "drop(untilOutputFrom:)") {
  // 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. 创建两个PassthroughSubject,你可以通过它手动发送值。第一个是isReady,第二个代表用户的点击。
  2. 使用drop(untilOutputFrom: isReady)来忽略用户的任何点击,直到isReady发出至少一个值。
  3. 通过主题发送五个"轻拍",就像上图中一样。在第三次敲击后,你向isReady发送一个值。

运行你的Playground,然后看一下你的调试控制台。你会看到下面的输出:

——— Example of: drop(untilOutputFrom:) ———
4
5

这个输出与上图相同:

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

你已经获得了摆脱不需要的值的相当高的技巧! 现在,是最后一组过滤操作符的时候了。限制值。

限制值

在上一节中,你已经学会了如何放弃--或跳过--值,直到满足某个条件。这个条件可以是与某个静态值相匹配,也可以是一个谓词闭包,或者是对不同发布者的依赖。

本节处理相反的需求:接收值直到满足某些条件,然后强迫发布者完成。例如,考虑一个可能发射未知数量的值的请求,但你只想要一个单一的发射,而不关心其他的。

Combineprefix系列的操作符解决了这一系列的问题。尽管这个名字并不完全直观,但这些操作符提供的能力对许多现实生活中的情况都很有用。

prefix系列操作符与drop系列相似,提供了prefix(_:)prefix(while:)prefix(untilOutputFrom:)。然而,prefix操作符不是在满足某些条件之前丢弃值,而是在满足该条件之前取值。

现在,是时候让你进入本章的最后一组操作符了,从refix(_:)开始。

作为dropFirst的反面,prefix(_:)将只取值到规定的数量,然后完成:

img

在你的Playground上添加以下代码来证明这一点:

example(of: "prefix") {
  // 1
  let numbers = (1...10).publisher

  // 2
  numbers
    .prefix(2)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

这段代码与你在上一节中使用的drop代码非常相似。你:

  1. 创建一个发布者,发出从110的数字。
  2. 使用prefix(2)来允许只发射前两个值。一旦有两个值被发射出来,发布者就完成了。

运行你的Playground,你会看到以下输出:

——— Example of: prefix ———
1
2
Completed with: finished

就像first(where:)一样,这个操作符是懒惰的,意味着它只占用它需要的值,然后终止。这也防止了numbers12之外产生额外的值,因为它也完成了。

接下来是prefix(while:),它接收一个谓词闭包,只要闭包的结果是true,就允许上游发布者的值通过。一旦结果为false,发布者就会完成:

img

在你的Playground上添加下面的例子来尝试这个:

example(of: "prefix(while:)") {
  // 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. 创建一个发送110之间数值的发布者。
  2. 使用prefix(while:)来让值通过,只要它们小于3。一旦有一个等于或大于3的值被发射出来,发布者就完成了。

运行Playground并查看调试控制台;输出应该与前面的操作者的输出相同:

——— Example of: prefix(while:) ———
1
2
Completed with: finished

在前两个prefix操作符之后,现在是最复杂的一个:prefix(untilOutputFrom:)。再一次,与drop(untilOutputFrom:)相比,prefix(untilOutputFrom:)在第二个发布者发布之前跳过数值。

想象一下这样的场景:你有一个按钮,用户只能点击两次。一旦发生两次点击,按钮上的进一步点击事件就应该被省略:

img

将本章的最后一个例子添加到你的Playground的末尾:

example(of: "prefix(untilOutputFrom:)") {
  // 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()
    }
  }
}

如果你回想一下drop(untilOutputFrom:)的例子,你应该发现这很容易理解。你:

  1. 创建两个PassthroughSubject,你可以通过它手动发送值。第一个是isReady,第二个代表用户的点击。
  2. 使用prefix(untilOutputFrom: isReady)来让敲击事件通过,直到isReady发出至少一个值。
  3. 通过主题发送五个"轻拍",与上图完全一致。在第二次敲击后,你向isReady发送一个值。

运行这个Playground。看一下控制台,你应该看到以下内容:

——— Example of: prefix(untilOutputFrom:) ———
1
2
Completed with: finished

挑战

你现在已经掌握了相当多的过滤知识。为什么不尝试一下简短的挑战呢?

挑战:过滤所有的东西

创建一个例子,发布一个从1100的数字集合,并使用过滤操作符来。

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

你的例子的输出应该产生以下数字,每行一个:

52 54 56 58 60 62 64 66 68 70

Note

在这个挑战中,你需要将多个操作符串联起来以产生所需的值。

你可以在projects/challenge/Final.playground中找到这个挑战的完整解决方案。

关键点

在本章中,你学到了以下内容。

  • 过滤操作符让你控制上游发布者发出的哪些值被发送到下游,发送到另一个操作符或订阅者那里。
  • 当你不关心值本身,而只想要一个完成事件时,ignoreOutput是你的朋友。
  • 寻找值是另一种过滤方式,你可以使用first(where:)last(where:)分别找到第一个或最后一个与提供的谓词相匹配的值。
  • First-style操作符是懒惰的;它们只取需要的值,然后完成。最后式操作符是贪婪的,在决定哪个值是最后一个满足条件的值之前,必须知道值的全部范围。
  • 你可以通过使用drop系列的操作符来控制在向下游发送数值之前,上游发布者发出的数值有多少被忽略。
  • 同样地,你可以通过使用prefix系列操作符来控制上游发布者在完成之前可以发布多少个值。

接下来去哪?

哇,这一章讲得真好! 你应该觉得自己是一个过滤大师,准备好以任何你想要的方式引导这些上游值。

有了转换和过滤操作符的知识,现在是时候让你进入下一章,学习另一组非常有用的操作符了:组合操作符。