Combine之Operator(Filtering elements元素过滤)

1,011 阅读5分钟

在响应式编程的世界中,我们需要一个数据筛子,这个比喻实在是太好了,我们真正想要的数据本来就应是去除杂质后的数据,本文主要讲解Combine框架中数据过滤的Operator。

compactMap/tryCompactMap

企业微信截图_d1381be5-bafa-4b8b-9bda-c264b0daebc4.png

compact是压缩的意思,因此,很容易理解,compactMap能够压缩数据流中的数据,如上图所示,它能够自动过滤空数据,像筛子一下,保留非空数据

_ = ["a", nil, "c"]
    .publisher
    .compactMap { value -> String? in
        value
    }
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

它的参数中的闭包支持返回一个可选类型的参数,因此,我们可以把传进来的数据直接返回,就这么简单的一个操作就实现了空值过滤。

如果你觉得compactMap不过如此,那就打错特错了,不要忽略它后边的Map关键词,这说明它不仅能够过滤空数据,还保留了Map的全部功能。

_ = ["123", nil, "456"]
    .publisher
    .compactMap { value -> Int? in
        guard let v = value else {
            return nil
        }
        return Int(v)
    }
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

compactMap的精髓在于它支持返回可选类型,当遇到数据有可能为nil的场景时,可以考虑。

tryCompactMapcompactMap的基础上,允许在闭包中抛出异常,在这里就不做详细介绍了。

filter/tryFilter

企业微信截图_2261b41d-bc11-460b-b8c6-22d6852fa007.png

企业微信截图_f10c8019-4bde-46ac-a3f8-a8f7844492e6.png

filter既简单又强大,从它的名字,我们就能够理解它的作用,当我们需要一个数据筛子的时候,就可以考虑它了。

filter接受一个闭包作为参数,该闭包返回一个Bool类型的值,只有结果为true的数据才会被保存下来。

想象一个场景,如果我们需要筛选出价格小于300元的酒店,使用filter能够轻松实现。

在本例中,我们筛选了数字大于8的所有结果,代码如下:

_ = [5, 10, 25]
    .publisher
    .filter { value in
        value > 8
    }
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

removeDuplicates/tryRemoveDuplicates

企业微信截图_6ea06df5-1b09-4a4b-9d9c-97fa4d6ad0c8.png

原则上来讲,在pipline中流动的数据可以是任何类型的,当然也有可能是重复的,当我们需要对这些数据去重的时候,就应该考虑removeDuplicates了。

正如上图所示,重复的数据已经被过滤掉,默认情况下,当我们使用.removeDuplicates()的时候,publisher输出的数据必须要实现Equatable协议,系统使用该协议的==来判断两个数据是否相等。

_ = [1, 1, 1, 2, 2, 3]
    .publisher
    .removeDuplicates()
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

当然,程序可以是很灵活的,如果publisher输出的数据不实现Equatable协议,也是可以的,如下图:

企业微信截图_0b3d1702-c48e-41c8-aad6-eca409ee4e6c.png

let students = [Student(age: 20, score: 80),
                Student(age: 20, score: 90),
                Student(age: 21, score: 90)]

_ = students
    .publisher
    .removeDuplicates(by: { first, second in
        first.age == second.age
    })
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

当一个Operator的参数是闭包时,它的灵活性就很强,它就是一个声明式的写法,在上边的代码中,我们使用age去重,我们当然也可以使用score去重。

tryRemoveDuplicates允许在闭包中throw异常,我们就不做多余解释了:

_ = students
    .publisher
    .tryRemoveDuplicates(by: { first, second in
        if first.age == 21 {
            throw MyError.customError
        }
        return first.age == second.age
    })
    .sink(receiveCompletion: { _ in
        print("结束了")
    }, receiveValue: { someValue in
        print(someValue)
    })

replaceEmpty/replaceError/replaceNil

企业微信截图_719d7970-9100-46a1-b2c5-ce182315ff6a.png

replaceEmpty的核心思想是为数据流提供一个默认值。

那么,什么才叫空的数据流呢?

当pipline最后收到.finish事件时,如果之前没有数据在管道中流动过,这种情况就称为空数据流。

如上图所示,黑色竖线代表结束事件,当上方的数据流收到结束事件后,并没有数据流动,因此触发replaceEmpty,返回100。

我们写一个简单的程序验证一下:

class ReplaceViewViewObservableObject: ObservableObject {
    var publiser = PassthroughSubject<Int, Never>()
    var cancellables = Set<AnyCancellable>()

    @Published var number: Int = 0

    init() {
        publiser
            .replaceEmpty(with: 100)
            .assign(to: \.number, on: self)
            .store(in: &cancellables)
    }

    func stop() {
        publiser.send(completion: Subscribers.Completion.finished)
    }
    
    func getRandomValue() {
        publiser.send(Int.random(in: 50..<1000))
    }
}
HStack(spacing: 20) {
      Text("当前数值: \(myObject.number)")

      Button("随机值") {
          myObject.getRandomValue()
      }

      Button("结束") {
          myObject.stop()
      }
  }
  .padding(20)
  .border(Color.red)

选择PassthroughSubject作为publisher的目的是我们可以主动控制数据和事件的发送时机。

运行效果如下:

Kapture 2020-09-24 at 10.30.56.gif

点击随机值按钮的时候,会生成一个随机的整数,当点击结束按钮的时候,并不会把数字改成100,说明只要管道中曾经有数据流过,就不会触发replaceEmpty

再看下边的效果:

Kapture 2020-09-24 at 10.33.42.gif

当首先点击结束按钮时,可以看到数值变成了100,之后就不再变化了,这是因为点击结束按钮就发送了.finish事件,数据流就终止了。


企业微信截图_23054407-5eb6-45fb-af4b-e43267d122c4.png

如上图所示,replaceError能够捕获pipline中的任何异常,并返回一个默认值。当我们的数据流模型不想接受任何Error的场景下,可以考虑使用replaceError

使用catch也能够实现replaceError同样的功能:

.catch { err in
    return Just(0)
}

比较这两种写法,明显replaceError的写法更加简单,但存在另外一种使用场景,当监听到Error后,我们想返回一个新的publisher的时候,就只能使用catch了。


企业微信截图_73eab9cd-40cd-4697-8c7a-d469b197819e.png

replaceNil已经非常简单了,它能够替换数据流中的空数据,如果想过滤掉数据,可以考虑使用compactMap或者tryCompactMap

这些能够实现replace功能的Operator,按理说应该尽可能地放到pipline靠后的位置。