在响应式编程的世界中,我们需要一个数据筛子,这个比喻实在是太好了,我们真正想要的数据本来就应是去除杂质后的数据,本文主要讲解Combine框架中数据过滤的Operator。
compactMap/tryCompactMap
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的场景时,可以考虑。
tryCompactMap在compactMap的基础上,允许在闭包中抛出异常,在这里就不做详细介绍了。
filter/tryFilter
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
原则上来讲,在pipline中流动的数据可以是任何类型的,当然也有可能是重复的,当我们需要对这些数据去重的时候,就应该考虑removeDuplicates了。
正如上图所示,重复的数据已经被过滤掉,默认情况下,当我们使用.removeDuplicates()的时候,publisher输出的数据必须要实现Equatable协议,系统使用该协议的==来判断两个数据是否相等。
_ = [1, 1, 1, 2, 2, 3]
.publisher
.removeDuplicates()
.sink(receiveCompletion: { _ in
print("结束了")
}, receiveValue: { someValue in
print(someValue)
})
当然,程序可以是很灵活的,如果publisher输出的数据不实现Equatable协议,也是可以的,如下图:
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
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的目的是我们可以主动控制数据和事件的发送时机。
运行效果如下:
点击随机值按钮的时候,会生成一个随机的整数,当点击结束按钮的时候,并不会把数字改成100,说明只要管道中曾经有数据流过,就不会触发replaceEmpty。
再看下边的效果:
当首先点击结束按钮时,可以看到数值变成了100,之后就不再变化了,这是因为点击结束按钮就发送了.finish事件,数据流就终止了。
如上图所示,replaceError能够捕获pipline中的任何异常,并返回一个默认值。当我们的数据流模型不想接受任何Error的场景下,可以考虑使用replaceError。
使用catch也能够实现replaceError同样的功能:
.catch { err in
return Just(0)
}
比较这两种写法,明显replaceError的写法更加简单,但存在另外一种使用场景,当监听到Error后,我们想返回一个新的publisher的时候,就只能使用catch了。
replaceNil已经非常简单了,它能够替换数据流中的空数据,如果想过滤掉数据,可以考虑使用compactMap或者tryCompactMap。
这些能够实现replace功能的Operator,按理说应该尽可能地放到pipline靠后的位置。