仅仅从字面意思来看,Operator的核心作用就是操作数据,理想情况下,借助Operator,我们可以执行数据相关的各种各样的任务,比如:映射,聚合,过滤,去重,延时等等。通过组合多个Operator,突破想象力。
Mapping这个词翻译过来就是映射的意思,在机器学习中,为了获取分类的超平面,需要把低维的数据映射到高维空间,这个例子非常的有意思,它需要你有超强的想象力,才能感受到它的绝妙之处。
左图表示原数据分布在一个平面之上,要想正确分类,并不是那么容易,如果把这些数据映射到更高维度的空间中,我们就能获取到一个平面,右图中的绿色超平面,已经实现了分类功能。
本篇文章讲解的scan,tryScan,map,tryMap和flatMap.就体现了上边提到的核心思想,他们都能够把数据从一种状态映射为另外一种状态。
scan
假设有这样一个需求, 我们需要统计学生的总成绩,很自然,我们写出了如下的代码:
func calculateTotalScore(_ scores: [Int]) -> Int {
var totalScore: Int = 0
for score in scores {
totalScore += score
}
return totalScore
}
代码没有任何问题,但在响应式编程的世界中,处理数据的思想不是这样的,它把数据想象成水流,在铺好的管道中流动,无需关心水什么时候流入管道。
_ = [3, 4, 5]
.publisher
.scan(0) { acc, current -> Int in
acc + current
}
.sink(receiveValue: { someValue in
print(someValue)
})
理解scan的一个重要关键词是数据累积,它具有记忆功能,只能记忆一个单位的数据,因此,它能够把上次计算后的结果保存起来,以便在下次计算的时候,获取到这个值。
从设计角度思考,有记忆功能的东西,一般都需要设置初始值。在上边代码中,我们把初始值设置为0。
通过观察上图,数据的流动一目了然,scan他是声明式的,它告诉我们,它做了什么?那么声明式的好处就是,你可以随意修改你要做什么,比如,把数据换成字符,scan就能实现收集字符的功能,如下图所示:
.scan(0, { prevVal, newValue -> Int in
return prevVal + newValue
})
tryScan
凡是像这种通过闭包来映射数据的Operator,他们都存在一个问题,闭包内的代码是我们手动实现的,就有可能抛出异常,比如,模型转换,获取权限等,在Combine框架中,一旦pipline收到Error,就会立刻结束,发送.failure事件。
在开发中,最常见的情况是,当触发了某个条件后,我们主动抛出异常,一般情况下,这个异常的类型是我们自定义的。
tryScan就在scan的基础之上,让我们能够在闭包中主动抛出立场。
_ = ["a", "b", "c"]
.publisher
.tryScan("") { acc, current -> String in
if current == "c" {
throw TryScanError.customError
}
return acc + current
}
.sink(receiveCompletion: { _ in
print("结束了")
}, receiveValue: { someValue in
print(someValue)
})
map/tryMap
scan能够收集数据,那么map和tryMap则真正映射数据,它把数据从某种状态转换为我们需要的状态。
比如,当我们使用[URLSession.dataTaskPublisher]获取网络数据的时候,它的返回值类型为(data: Data, response: URLResponse),我们往往只需要Data,那么我们就可以使用map来转换数据:
.map { $0.data }
同tryScan一样,tryMap允许闭包中抛出异常
_ = [Student(name: "小明"), Student(name: "小红"), Student(name: "李雷")]
.publisher
.tryMap { value -> String in
if value.name == "李雷" {
throw MapAndTryMapError.customError
}
return value.name
}
.sink(receiveCompletion: { _ in
print("结束了")
}, receiveValue: { someValue in
print(someValue)
})
flatMap
flatMap是一个非常强大的Operator,它能够把上游输出的值转换为一个新的pulisher,这就很牛了。
我们都知道,pulisher可以是链式的,它本身就是一个完整的管道,配合flatMap和pulisher,我们就能够设计出非常强大的pipline。
举个例子:
enum MapAndTryMapError: Error {
case customError
}
struct Student: Decodable {
let name: String
}
let json = """
[{
"name": "小明"
},
{
"name": "小红"
},
{
"name": "李雷"
}]
"""
_ = Just(json)
.flatMap { value in
Just(value.data(using: .utf8)!)
.decode(type: [Student].self, decoder: JSONDecoder())
.catch { _ in
Just([Student(name: "无名氏")])
}
}
.sink(receiveCompletion: { _ in
print("结束了")
}, receiveValue: { someValue in
print(someValue)
})
在上边的代码中,把json字符串通过flatMap映射为[Student]。在模型序列化的过程中,可能会出错,当catch到异常后,我们返回一个默认的值。
再次强调:flatMap的强大之处在于,它的返回值是一个Publisher。
setFailureType
正如上图所示,setFailureType并不会发送一个.failure事件,它仅仅改变了publisher输出的错误类型。
其实,它的使用场景并不多,很多时候我们可以使用带.try开头的operator来实现相同的功能,比如说用.tryMap来抛出自定义类型的Error。
我们先看看本例中.timeout的定义:
extension Publisher {
public func timeout<S>(_ interval: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil, customError: (() -> Self.Failure)? = nil) -> Publishers.Timeout<Self, S> where S : Scheduler
}
可以看出,它有一个customError的参数,当超时触发后,它被调用。它是一个闭包,返回值为Self.Failure,也就是上游返回的错误类型。完整代码如下:
enum SetFailureTypeError: Error {
case customError
}
init() {
_ = ["a", "b", "c"]
.publisher
.setFailureType(to: SetFailureTypeError.self)
.timeout(0.1, scheduler: DispatchQueue.main) {
SetFailureTypeError.customError
}
.sink(receiveCompletion: { _ in
print("结束了")
}, receiveValue: { someValue in
print(someValue)
})
}
由于customError返回的错误类型为SetFailureTypeError.customError与上游的Never不相符,因此编译器会报这样的错误:
Cannot convert value of type 'SetFailureTypeTest.SetFailureTypeError' to closure result type 'Never'
总结一下,当需要对publisher的Error输出类型做映射的时候,考虑使用setFailureType。