Combine之Operator[Mapping elements元素映射]

1,112 阅读5分钟

github.com/agelessman/…

仅仅从字面意思来看,Operator的核心作用就是操作数据,理想情况下,借助Operator,我们可以执行数据相关的各种各样的任务,比如:映射,聚合,过滤,去重,延时等等。通过组合多个Operator,突破想象力。

Mapping这个词翻译过来就是映射的意思,在机器学习中,为了获取分类的超平面,需要把低维的数据映射到高维空间,这个例子非常的有意思,它需要你有超强的想象力,才能感受到它的绝妙之处。

bff9286c9f4c802c725fae9f8ad9f740.jpg-article.jpeg

左图表示原数据分布在一个平面之上,要想正确分类,并不是那么容易,如果把这些数据映射到更高维度的空间中,我们就能获取到一个平面,右图中的绿色超平面,已经实现了分类功能。

本篇文章讲解的scantryScanmaptryMapflatMap.就体现了上边提到的核心思想,他们都能够把数据从一种状态映射为另外一种状态。

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。

企业微信截图_ece221de-6534-4045-adf0-571392c3fe0b.png

通过观察上图,数据的流动一目了然,scan他是声明式的,它告诉我们,它做了什么?那么声明式的好处就是,你可以随意修改你要做什么,比如,把数据换成字符,scan就能实现收集字符的功能,如下图所示:

企业微信截图_f5de4201-862a-44d6-b8d9-ca0dd8ab1e47.png

.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)
})

企业微信截图_3dc64e3e-3dec-4b0f-91e8-37246f5c8af7.png

map/tryMap

scan能够收集数据,那么maptryMap则真正映射数据,它把数据从某种状态转换为我们需要的状态。

比如,当我们使用[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)
    })

企业微信截图_c4e9d095-ea01-4451-8b6c-f770fd22ae82.png

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

企业微信截图_a77855a3-1065-4cb8-9717-b230619cb0dc.png

setFailureType

企业微信截图_1526f02a-f2bc-4011-ab9e-35de2bb664c9.png

正如上图所示,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。