Kotlin Flow的GroupBy运算符

时间:2019-10-30 12:21:49

标签: kotlin kotlin-coroutines flow

我正在尝试从RxJava切换到Kotlin Flow。流量确实令人印象深刻。但是,现在在kotlin Flow中是否有类似于RxJava的“ GroupBy”的运算符?

2 个答案:

答案 0 :(得分:2)

从Kotlin Coroutines 1.3开始,标准库似乎未提供此运算符。但是,由于Flow的设计使得所有运算符都是扩展函数,因此提供它的标准库与您自己编写的标准库之间没有根本区别。

牢记这一点,这是我关于如何处理它的一些想法。

1。将每个组收集到列表中

如果您只需要每个键的所有项目的列表,请使用此简单的实现来生成(K, List<T>)对:

fun <T, K> Flow<T>.groupToList(getKey: (T) -> K): Flow<Pair<K, List<T>>> = flow {
    val storage = mutableMapOf<K, MutableList<T>>()
    collect { t -> storage.getOrPut(getKey(t)) { mutableListOf() } += t }
    storage.forEach { (k, ts) -> emit(k to ts) }
}

对于此示例:

suspend fun main() {
    val input = 1..10
    input.asFlow()
            .groupToList { it % 2 }
            .collect { println(it) }
}

它打印

(1, [1, 3, 5, 7, 9])
(0, [2, 4, 6, 8, 10])

2.a为每个组发送一个流

如果您需要完整的RxJava语义,可以将输入流转换为许多输出流(每个不同的键一个),那么事情就变得更加复杂。

每当在输入中看到新的键时,都必须向下游发出新的内部流,然后异步地在遇到相同的键时继续向其中推送更多数据。

这是一个执行此操作的实现:

fun <T, K> Flow<T>.groupBy(getKey: (T) -> K): Flow<Pair<K, Flow<T>>> = flow {
    val storage = mutableMapOf<K, SendChannel<T>>()
    try {
        collect { t ->
            val key = getKey(t)
            storage.getOrPut(key) {
                Channel<T>(32).also { emit(key to it.consumeAsFlow()) }
            }.send(t)
        }
    } finally {
        storage.values.forEach { chan -> chan.close() }
    }
}

它为每个键设置一个Channel,并将通道作为流向下游公开。

2.b同时收集和减少分组流量

由于groupBy在将流本身发送到下游之后一直向内部流发送数据,因此在收集数据时必须非常小心。

您必须同时收集所有内部流,并且并发级别没有上限。否则,排队等待稍后收集的流的通道最终将阻止发件人,您将最终陷入僵局。

以下是可以正确执行此操作的函数:

fun <T, K, R> Flow<Pair<K, Flow<T>>>.reducePerKey(
        reduce: suspend Flow<T>.() -> R
): Flow<Pair<K, R>> = flow {
    coroutineScope {
        this@reducePerKey
                .map { (key, flow) -> key to async { flow.reduce() } }
                .toList()
                .forEach { (key, deferred) -> emit(key to deferred.await()) }
    }
}

map阶段为其收到的每个内部流启动一个协程。协程将其减少到最终结果。

toList()是一个终端操作,它收集整个上游流,并启动该过程中的所有async协程。即使我们仍在收集主流,协程也会开始消耗内部流。这对于防止死锁至关重要。

最后,在所有协程启动之后,我们开始一个forEach循环,等待并发出最终结果(当它们可用时)。

对于flatMapMerge,您可以实现几乎相同的行为:

fun <T, K, R> Flow<Pair<K, Flow<T>>>.reducePerKey(
        reduce: suspend Flow<T>.() -> R
): Flow<Pair<K, R>> = flatMapMerge(Int.MAX_VALUE) { (key, flow) ->
    flow { emit(key to flow.reduce()) }
}

区别在于顺序:第一个实现尊重输入中键的出现顺序,而第一个实现则不这样做。两者的表现相似。

3。例子

此示例对4000万个整数进行分组和求和:

suspend fun main() {
    val input = 1..40_000_000
    input.asFlow()
            .groupBy { it % 100 }
            .reducePerKey { sum { it.toLong() } }
            .collect { println(it) }
}

suspend fun <T> Flow<T>.sum(toLong: suspend (T) -> Long): Long {
    var sum = 0L
    collect { sum += toLong(it) }
    return sum
}

我可以使用-Xmx64m成功运行它。在我的4核笔记本电脑上,我每秒可获得约400万个物品。

很容易根据新的解决方案重新定义第一个解决方案,如下所示:

fun <T, K> Flow<T>.groupToList(getKey: (T) -> K): Flow<Pair<K, List<T>>> =
        groupBy(getKey).reducePerKey { toList() }

答案 1 :(得分:0)

还没有,但是您可以看看这个库https://github.com/akarnokd/kotlin-flow-extensions