关于kotlin中的Collections、Sequence、Channel和Flow (一)

前言

随着StateFlow/SharedFlow的稳定, Coroutine Flow 功能也越来越强大,那么让我们来看看实际开发中能用它来解决什么问题。在此之前还要介绍下CollectionsSequenceChannel它们虽然是解决不同问题的事物,但是它们之间却有一些隐性联系。

Collections

image.png

简介

CollectionsKotlin标准库提供的一整套用于管理集合的工具,常见的集合类型有:

  • List 是一个有序集合,可通过索引(反映元素位置的整数)访问元素。元素可以在 list 中出现多次。列表的一个示例是一句话:有一组字、这些字的顺序很重要并且字可以重复。

  • Set 是唯一元素的集合。它反映了集合(set)的数学抽象:一组无重复的对象。一般来说 set 中元素的顺序并不重要。例如,字母表是字母的集合(set)。

  • Map(或者字典)是一组键值对。键是唯一的,每个键都刚好映射到一个值。值可以重复。map 对于存储对象之间的逻辑连接非常有用,例如,员工的 ID 与员工的位置。

//list
val stringList = listOf("one", "two", "one")

//set
val stringSet  = setOf("one", "two", "three")

//map
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
复制代码

操作

Kotlin 标准库提供了用于对集合执行操作的多种函数。这包括简单的操作,例如获取或添加元素,以及更复杂的操作,包括搜索、排序、过滤、转换等。

listOf(2, 1, 4, 7)
        .map {
            println("map $it")
            it * 2
        }
        .filter {
            println("filter $it")
            it > 2
        }
        .take(2).forEach {
            println("final $it-------")
        }
log:
//排序之后
map 1
map 2
map 4
map 7
filter 2
filter 4
filter 8
filter 14
final 4-------
final 8-------
复制代码

以上每个处理步骤会创建额外的集合来保存过程中产生的中间结果:

//map
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

//filter
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
复制代码

这里最后只需要2个数据,但每个数据都进行了处理,造成了一定的浪费。怎么办呢?

Sequence

image.png

简介

Kotlin 标准库还包含另一种容器类型——序列(Sequence<T>)。 序列提供与 Iterable 相同的函数,但使用另一种方法来进行多步骤集合处理。

Sequence 对每个元素逐个执行所有处理步骤,并且是惰性的。

操作

当我们将集合转换为sequence之后再进行操作:

listOf(2, 1, 4, 7)
        .asSequence()
        .map {
            println("map $it")
            it * 2
        }
        .filter {
            println("filter $it")
            it > 2
        }
        .take(2).forEach {
            println("final $it-------")
        }
        
        
输出:
map 1
filter 2
map 2
filter 4
final 4-------
map 4
filter 8
final 8-------
复制代码

可以看到是逐个执行所有操作,但性能提升也是相对。

惰性

序列操作分为两个过程:中间操作、末端操作。中间操作全部都是惰性操作:如果没有执行末端操作,中间操作都不会被执行。

           | ----- 中间操作 ----- |
sequence.map { ... }.filter { ... }.forEach{}
                                  |-末端操作-|
复制代码

Show code:

val list = listOf(1, 2, 3, 4)
list.asSequence().filter { println("first:$it");it > 2 }
list.asSequence().filter { println("second:$it");it > 2 }.toList()

print:
second:1
second:2
second:3
second:4
复制代码

可以看到不加toList()的是不会有任何结果的,只有加上了toList()才会执行print操作,也就是为什么说中间操作是惰性的,在没有末端操作的时候,中间操作会被延期执行。

  • 如果序列操作返回延迟生成的另一个序列,称为 中间序列。 否则该操作为 末端操作。

我们可以把这些操作符称之为末端操作符。 所以也可以说Sequence是冷的?。cold or lazy

  • 这些序列可避免生成中间步骤的结果,从而提高了整个集合处理链的性能。 但是,序列的延迟性质增加了一些开销,这些开销在处理较小的集合或进行更简单的计算时可能很重要。 因此,应该同时考虑使用 SequenceIterable,并确定在哪种情况更适合。

  • 原理是所有操作封装进同一个Iterator ,末端操作执行 next 就跑起来了

关于性能分析是要看情况的:

Kotlin : Slow List and Lazy Sequence

[译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?

构建

以上都是由Iterable对象 调用asSequence进行转换的,那么还有其他构建方式吗?

由元素

val numbersSequence = sequenceOf("four", "three", "two", "one")
复制代码

Iterable

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
复制代码

由函数

要基于函数构建序列,请以该函数作为参数调用 generateSequence()。 (可选)可以将第一个元素指定为显式值或函数调用的结果。 当提供的函数返回 null 时,序列生成停止.
以下示例中的序列是无限的:

val oddNumbers = generateSequence(1) { it + 2 } // `it` 是上一个元素
println(oddNumbers.take(5).toList())
//println(oddNumbers.count())     // 错误:此序列是无限的。

输出:
[1,3,5,7,9]
复制代码

要使用 generateSequence() 创建有限序列,请提供一个函数,该函数在需要的最后一个元素之后返回 null

val oddNumbers = generateSequence(1) {
    if (it > 5) {
        null
    } else {
        it + 2
    }
} // it 是上一个元素
println(oddNumbers.take(5).toList())
输出:
[1,3,5,7]
复制代码

由代码块

以上的构造感觉都不够灵活,怎么自由添加元素呢?
kotlin 提供了一个函数可以逐个或按任意大小的组块生成序列元素——sequence() 函数。 此函数采用一个 lambda 表达式,其中包含 yield()yieldAll() 函数的调用。

val oddNumbers = sequence {
    println("Start sequence build...")

    yield(1)

    println("A")

    yieldAll(listOf(3, 5))

    println("B")

    yieldAll(generateSequence(7) { it + 2 })

    println("C")

}
println(oddNumbers.take(5).toList())

输出:
Start sequence build...
A
B
[1, 3, 5, 7, 9]
复制代码

yiledyiledAll它们将元素返回给序列使用者,并挂起sequence() 的执行,直到使用者请求下一个元素。 yield() 使用单个元素作为参数;yieldAll() 中可以采用 Iterable 对象、Iterable 或其他 Sequence

可以看到发送数据的方法是suspend方法:

public abstract suspend fun yield(value: T)

public abstract suspend fun yieldAll(iterator: Iterator<T>)
复制代码

Kotlin 编译器会把suspend函数转化为有限状态机(算是一种优化版的回调)。这样做的好处就是只有当序列的迭代器的调用next()后,才会去切回挂起方法里执行值的发送。
Sequence 某种意义上更像是生产 – 消费者模型中的生产者,而迭代序列的一方则像是消费者。

那么我能在里面调用其他suspend方法吗?很遗憾并不能:

@RestrictsSuspension
@SinceKotlin("1.3")
public abstract class SequenceScope<in T> internal constructor() {}
复制代码

RestrictsSuspension

带有此注释的类和接口将受到限制,不能随意调用挂起函数。

SequenceBuilder 其并非为异步流设计,Sequence用于同步流没什么问题,那么针对异步流我们该如何处理呢?

答案下期揭晓 : 关于kotlin中的Collections、Sequence、Channel和Flow (二)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享