Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

序列

Kotlin 的 Sequence 类似于 List,但您只能迭代遍历 Sequence,不能对 Sequence 进行索引。这个限制产生了非常高效的链式操作。

在其他函数式语言中,Kotlin 的 Sequence 被称为 (stream)。Kotlin 必须选择一个不同的名称,以保持与 Java 8 的 Stream 库的互操作性。

List 的操作是急切(eager)执行的,它们总是立即执行的。在链接 List 操作时,必须在开始下一个操作之前生成第一个结果。在下面的示例中,每个 filter()map()any() 操作都应用于 list 中的每个元素:

// Sequences/EagerEvaluation.kt
import atomictest.eq

fun main() {
  val list = listOf(1, 2, 3, 4)

  list.filter { it % 2 == 0 }
    .map { it * it }
    .any { it < 10 } eq true

  // 相当于:
  val mid1 = list.filter { it % 2 == 0 }
  mid1 eq listOf(2, 4)
  val mid2 = mid1.map { it * it }
  mid2 eq listOf(4, 16)
  mid2.any { it < 10 } eq true
}

急切求值是直观而简单的,但可能不太优化。在 EagerEvaluation.kt 中,遇到满足 any() 的第一个元素后停止会更有意义。对于长序列,这种优化可能比计算每个元素然后搜索单个匹配要快得多。

急切求值有时被称为水平求值

horizontal evaluation

水平求值

第一行包含初始列表内容。每一行后面都显示了上一个操作的结果。在执行下一个操作之前,会处理当前水平级别上的所有元素。

急切求值的替代方案是惰性求值:只有在需要时才计算结果。在序列上执行惰性操作有时被称为垂直求值

vertical evaluation

垂直求值

使用惰性求值,仅当请求某个元素的关联结果时才对该元素执行操作。如果在处理最后一个元素之前找到了计算的最终结果,那么不会再处理更多的元素。

通过使用 asSequence()List 转换为 Sequence,可以实现惰性求值。除索引之外,所有 List 操作也适用于 Sequence,因此通常可以进行这个单一的改变,并获得惰性求值的好处。

下面的示例将上述图示转换为代码。我们首先在 List 上执行相同的一系列操作,然后在 Sequence 上执行。输出显示了每个操作的调用位置:

// Sequences/EagerVsLazyEvaluation.kt
package sequences
import atomictest.*

fun Int.isEven(): Boolean {
  trace("$this.isEven()")
  return this % 2 == 0
}

fun Int.square(): Int {
  trace("$this.square()")
  return this * this
}

fun Int.lessThanTen(): Boolean {
  trace("$this.lessThanTen()")
  return this < 10
}

fun main() {
  val list = listOf(1, 2, 3, 4)
  trace(">>> List:")
  trace(
    list
      .filter(Int::isEven)
      .map(Int::square)
      .any(Int::lessThanTen)
  )
  trace(">>> Sequence:")
  trace(
    list.asSequence()
      .filter(Int::isEven)
      .map(Int::square)
      .any(Int::lessThanTen)
  )
  trace eq """
    >>> List:
    1.isEven()
    2.isEven()
    3.isEven()
    4.isEven()
    2.square()
    4.square()
    4.lessThanTen()
    true
    >>> Sequence:
    1.isEven()
    2.isEven()
    2.square()
    4.lessThanTen()
    true
  """
}

两种方法之间唯一的区别是添加了 asSequence() 调用,但是 List 代码会处理更多的元素。

Sequence 上调用 filter()map() 会产生另一个 Sequence。在从计算中请求结果之前,什么都不会发生。相反,新的 Sequence 存储了有关延迟操作的所有信息,并且只有在需要时才执行这些操作:

// Sequences/NoComputationYet.kt
import atomictest.eq
import sequences.*

fun main() {
  val r = listOf(1, 2, 3, 4)
    .asSequence()
    .filter(Int::isEven)
    .map(Int::square)
  r.toString().substringBefore("@") eq
    "kotlin.sequences.TransformingSequence"
}

r 转换为字符串不会产生所需的结果,而只会产生对象的标识符(包括内存中对象的 @ 地址,我们使用标准库的 substringBefore() 删除它)。TransformingSequence 只保存操作,但不执行它们。

Sequence 操作分为两类:中间操作终端操作。中间操作返回另一个 Sequence 作为结果。filter()map() 是中间操作。终端操作返回一个非 Sequence。为了实现这一点,终端操作会执行所有存储的计算。在前面的示例中,any() 是终端操作,因为它接受一个 Sequence 并返回一个 Boolean。在下面的示例中,`

toList()是终端操作,因为它将Sequence转换为List`,在此过程中运行所有存储的操作:

// Sequences/TerminalOperations.kt
import sequences.*
import atomictest.*

fun main() {
  val list = listOf(1, 2, 3, 4)
  trace(list.asSequence()
    .filter(Int::isEven)
    .map(Int::square)
    .toList())
  trace eq """
    1.isEven()
    2.isEven()
    2.square()
    3.isEven()
    4.isEven()
    4.square()
    [4, 16]
  """
}

由于 Sequence 存储操作,它可以以任何顺序调用这些操作,从而实现惰性求值。

下面的示例使用标准库函数 generateSequence() 生成一个无限自然数序列。第一个参数是序列中的初始元素,后面跟着一个定义如何从上一个元素计算下一个元素的 lambda:

// Sequences/GenerateSequence1.kt
import atomictest.eq

fun main() {
  val naturalNumbers =
    generateSequence(1) { it + 1 }
  naturalNumbers.take(3).toList() eq
    listOf(1, 2, 3)
  naturalNumbers.take(10).sum() eq 55
}

Collection 具有已知大小,可以通过其 size 属性发现。Sequence 被视为无限大。在这里,我们使用 take() 来决定需要多少个元素,然后是终端操作(toList()sum())。

generateSequence() 还有一个重载版本,不需要第一个参数,只需要一个返回序列中下一个元素的 lambda。当没有更多元素时,它会返回 null。下面的示例生成一个 Sequence,直到输入中出现“终止标志” XXX

// Sequences/GenerateSequence2.kt
import atomictest.*

fun main() {
  val items = mutableListOf(
    "first", "second", "third", "XXX", "4th"
  )
  val seq = generateSequence {
    items.removeAt(0).takeIf { it != "XXX" }
  }
  seq.toList() eq "[first, second, third]"
  capture {
    seq.toList()
  } eq "IllegalStateException: This " +
    "sequence can be consumed only once."
}

removeAt(0)List 中删除并生成零索引位置的元素。takeIf() 如果满足给定的谓词,则返回接收者(由 removeAt(0) 生成的 String),如果谓词失败(当 String"XXX" 时),则返回 null

您只能对 Sequence 进行一次迭代。进一步的尝试会产生异常。要通过 Sequence 进行多次遍历,请首先将其转换为某种类型的 Collection

下面是 takeIf() 的实现,使用泛型 T 定义,以便可以与任何类型的参数一起使用:

// Sequences/DefineTakeIf.kt
package sequences
import atomictest.eq

fun <T> T.takeIf(
  predicate: (T) -> Boolean
): T? {
  return if (predicate(this)) this else null
}

fun main() {
  "abc".takeIf { it != "XXX" } eq "abc"
  "XXX".takeIf { it != "XXX" } eq null
}

在这里,generateSequence()takeIf() 生成一个递减的数字序列:

// Sequences/NumberSequence2.kt
import atomictest.eq

fun main() {
  generateSequence(6) {
    (it - 1).takeIf { it > 0 }
  }.toList() eq listOf(6, 5, 4, 3, 2, 1)
}

普通的 if 表达式始终可以用作 takeIf() 的替代方案,但是引入额外的标识符可能会使 if 表达式变得笨拙。takeIf() 版本更加功能化,特别是如果它作为一系列调用的一部分使用。

练习和解答可以在 www.AtomicKotlin.com 找到。