Edit Page

序列(Sequence)

最终更新: 2024/03/21

除集合之外, Kotlin 还提供了另一种类型 – 序列(sequence) (Sequence<T>). 与集合(Collection)不同, 序列并不包含元素, 而是在迭代时生成元素. 序列提供的函数和 Iterable 一样, 但对多步骤的集合处理提供另一种实现方式.

当对 Iterable 的处理包含多个步骤时, 会以及早计算(eager)模式执行: 每个处理步骤都会执行完毕, 并返回它的结果 – 也就是一个中间集合. 然后再对这个中间集合执行下一个步骤. 与此不同, 对序列的多步骤处理会尽量以延迟计算(lazy)模式执行: 只有在整个处理链的结果真正被使用到时, 才会执行相应的计算处理.

操作的执行顺序也不同: Sequence 对每个元素执行所有的处理步骤. 而 Iterable 会对整个集合执行单个处理步骤, 然后再对结果集合执行下一个处理步骤.

通过这种方式, 序列可以避免生成各个处理步骤的中间结果, 因此能够提高集合多步骤处理的整体性能. 然而, 序列的延迟计算(lazy)模式会增加一些开销, 在处理小集合, 或进行简单计算时, 这些开销可能会比较显著. 因此, 应该同时考虑使用 SequenceIterable, 根据你的具体情况决定哪一个比较好.

创建序列

通过指定的元素创建

要创建序列, 可以使用 sequenceOf() 函数, 通过函数参数指定序列中的元素.

val numbersSequence = sequenceOf("four", "three", "two", "one")

通过 Iterable 创建

如果你已经有了一个 Iterable 对象 (比如 ListSet), 你可以调用它的 asSequence(). 函数来创建序列.

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

通过函数创建

创建序列的另一种方式是, 通过一个函数来计算序列中的元素. 调用 generateSequence() 函数, 把序列元素的计算函数作为它的参数, 这样就可以通过函数来创建序列. 这个函数有一个可选的参数, 你可以明确指定第一个元素的值, 也可以指定一个函数来计算第一个元素的值. 当序列元素的计算函数返回 null 时, 序列的生成过程会停止. 所以, 下面示例中的序列是无限的.


fun main() {
//sampleStart
    val oddNumbers = generateSequence(1) { it + 2 } // `it` 是前一个元素
    println(oddNumbers.take(5).toList())
    //println(oddNumbers.count())     // 这里会发生运行时错误: 序列是无限的
//sampleEnd
}

如果想要使用 generateSequence() 函数创建有限的序列, 那么你的序列元素的计算函数应该在生成最后一个元素之后返回 null.


fun main() {
//sampleStart
    val oddNumbersLessThan10 = generateSequence(1) { if (it < 8) it + 2 else null }
    println(oddNumbersLessThan10.count())
//sampleEnd
}

通过数据块(chunk)创建

最后, 还有 sequence() 函数, 可以逐个生成序列元素, 或者通过任意大小的数据块来生成元素. 这个函数的参数是一个 lambda 表达式, 其中包括对 yield() 函数和 yieldAll() 函数的调用. 这些函数会将元素返回给序列的使用者, 然后暂停 sequence() 函数的执行, 直到序列使用者请求下一个元素. yield() 的参数是单个元素; yieldAll() 的参数可以是一个 Iterable 对象, 或一个 Iterator, 或另一个 Sequence. yieldAll() 函数的 Sequence 参数可以是无限的. 但是, 这样的调用必须出现在序列的最末尾: 否则, 在这之后的所有序列元素都不会被执行到.


fun main() {
//sampleStart
    val oddNumbers = sequence {
        yield(1)
        yieldAll(listOf(3, 5))
        yieldAll(generateSequence(7) { it + 2 })
    }
    println(oddNumbers.take(5).toList())
//sampleEnd
}

序列的操作

根据对数据状态的要求不同, 序列的操作可以分为以下两类:

  • 无状态(Stateless) 操作, 不需要保存状态信息, 对每个元素的处理都是独立的, 比如, map()filter(). 无状态操作本身可以使用固定数量的少量状态数据来处理一个元素, 比如, take()drop().
  • 有状态(Stateful) 操作, 需要大量的状态信息, 通常正比于序列内的元素数量.

如果序列的一个操作返回另一个序列, 结果序列的内容是延迟计算的, 我们称这个操作为 中间(intermediate) 操作. 相反, 如果一个操作不返回新的序列, 那么称为 终止(terminal) 操作. 终止操作的例子, 比如 toList()sum(). 只有执行终止操作后, 才能取得序列中的元素.

序列元素可以多次遍历; 序列的某些实现类可能造成限制, 使得它只能遍历一次. 这样的限制会在这些序列的文档中明确说明.

序列处理的例子

下面我们通过一个例子来看看 IterableSequence 区别.

使用 Iterable

假设你有很多单词. 下面的代码会过滤长度超过 3 个字母的单词, 然后打印前 4 个这种单词的长度.


fun main() {    
//sampleStart
    val words = "The quick brown fox jumps over the lazy dog".split(" ")
    val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
        .map { println("length: ${it.length}"); it.length }
        .take(4)

    println("Lengths of first 4 words longer than 3 chars:")
    println(lengthsList)
//sampleEnd
}

运行这段代码时, 你可以看到 filter()map() 函数的执行顺序与它们在代码中出现的顺序相同. 首先, 你会看到对所有元素输出 filter:, 然后对过滤之后剩余的元素输出 length:, 然后是最后两行代码的输出.

下图是 list 各处理步骤的具体执行过程:

List processing

使用序列

下面我们用序列来实现同样的处理:


fun main() {
//sampleStart
    val words = "The quick brown fox jumps over the lazy dog".split(" ")
    // 将 List 转换为序列
    val wordsSequence = words.asSequence()

    val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
        .map { println("length: ${it.length}"); it.length }
        .take(4)

    println("Lengths of first 4 words longer than 3 chars")
    // 终止操作: 以 List 形式获取结果
    println(lengthsSequence.toList())
//sampleEnd
}

这段代码的输出显示, filter()map() 函数在创建最终的结果 list 时才被执行. 因此, 你首先看到的输出是 "Lengths of..", 然后序列的处理才会开始. 注意, 对于过滤之后剩余的元素, 会在过滤下一个元素之前执行 map 操作. 当结果的元素数量到达 4 时, 处理将会停止, 因为这是 take(4) 能够返回的最大元素数量.

序列的处理过程如下:

Sequences processing

在这个示例中, 序列处理执行了 18 步, 而使用 list 时则需要 23 步.