Combine之Operator(Reducing elements元素减少)

1,046 阅读4分钟

github.com/agelessman/…

reducing这个词是减少的意思,但在Combine中,它体现的核心思想确是数据收集,在上篇文章Combine之Operator(Filtering elements元素过滤)中,对于某个序列中的数据,我们使用过滤算法,可以实现让数据减少的目的,也就是说,数据在管道中流动,在水龙头处,我们获取的是过滤后的数据。

假如有下边这样的需求:

  • 需要等待收集完管道中所有的数据后,再处理后边的逻辑
  • 更进一步,我们自定义数据收集的方法

因此,reduce体现了数据收集的核心思想,在本文中,我们将看到它在数据收集上的价值。

collect

image.png

如上图所示,collect表达了收集的核心思想,collect一共有4种不同的用法:

  • .collect(),当没有任何参数时,它会收集publisher发送地所有数据,直到收到.finished事件。相信大家应该也意识到了,这种用法可能会造成内存方面的问题,collect会把publisher发送的数据保存在内存之中,因此,这种情况需要考虑数据量的问题。
  • .collect(3),当给其一个count参数时,它会按照指定的个数来收集数据,这就避免了内存溢出的问题。我们称这种收集数据的维度为空间维度。比如,我们上图中设置的count为3,当数据满3个的时候就会收集一次。如果在收到.finished事件之后个数不满3个,则先发送剩余的数据,然后结束pipline。
  • .collect(.byTime(q, 1.0)),试想,收集数据的维度不仅仅只有空间维度,还有时间维度,我们可以收集某个时间间隔内的数据,这就是byTime这个参数的用处
  • .collect(.byTimeOrCount(q, 1.0, 5)),如果在时间维度上再加上空间维度就更完美了

给大家看一下collect(3)收集数据的动画过程:

Kapture 2020-10-09 at 10.50.10.gif

ignoreOutput

image.png

ignoreOutput能够忽略publisher发送的数据。

当我们只想监听pipline的完成事件的情况下,可以考虑使用ignoreOutput。比如,当数组中的所有数据都发送完成后,我们打印这个完成事件,代码如下:

_ = ["1", "2", "3"]
    .publisher
    .ignoreOutput()
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
            break
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
            break
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

大家把上边的代码和上边的图片结合起来看,在整个过程中,不会打印someValue。

reduce

image.png

reduce的写法跟scan很像,不同之处在于,它按照闭包的规则把数据写到内存中,只有当publisher发送数据完毕后,才会把收集到的所有数据一次性发出去。

像具有这样功能的Operator,都需要提供一个默认的初始值,在上边的图片中可以看出,我们用的初始值为一个空的数组,收集的逻辑为:每次收到一个新值,就把它拼接到原有的数组中,这样就保证了最后收集到的数据是一个完整的数组。

代码如下:

_ = [1, 2, 3, 4, 5, 6, 7, 8]
    .publisher
    .reduce([], { preValue, newValue -> [Int] in
        return preValue + [newValue]
    })
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
            break
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
            break
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

我们反复强调声明式编程思想的目的是让大家理解这种编程思想的巨大优势,在上边的代码中,.reduce接受的参数是一个闭包,这就说明我们可以随意为reduce指定策略,比如,在上边的代码中,我们的策略是收集全部的数据,我们只需对代码做一点点修改就可以把策略改为只收集偶数:

.reduce([], { preValue, newValue -> [Int] in
    return newValue % 2 == 0 ? preValue + [newValue] : preValue
})

image.png

当然,凡是用到闭包的地方,必然会有一个tryReduce,它允许我们在闭包中抛出异常,一旦抛出异常,pipeline就会立刻结束,sink就会收到错误信息,代码如下:

enum MyCustomError: Error {
    case customError
}

...
.tryReduce([], { preValue, newValue -> [Int] in
    if newValue == 6 {
        throw MyCustomError.customError
    }
    return newValue % 2 == 0 ? preValue + [newValue] : preValue
})
...

这里省略了一些重复代码,只显示了差异的部分。当收到的值为6的时候,抛出异常,pipline终止,示意图如下:

image.png