Kotlin的Iterable和Sequence看起来完全一样。为什么需要两种类型?


Answers:


136

关键的区别在于语义和的STDLIB扩展功能的实现Iterable<T>Sequence<T>

  • 对于Sequence<T>,扩展功能在可能的情况下延迟执行,类似于Java Streams中间操作。例如,Sequence<T>.map { ... }返回另一个Sequence<R>,并且直到调用诸如或的终端操作之前,它才真正处理项目。toListfold

    考虑以下代码:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    它打印:

    before sum 1 2
    

    Sequence<T>当您希望减少终端中完成的工作时,可用于延迟使用和高效的流水线操作操作中(与Java Streams相同),它操作。但是,懒惰会带来一些开销,这对于较小的collection的常见简单转换是不希望的,并且会使它们的性能降低。

    通常,没有确定何时需要它的好方法,因此在Kotlin中,将stdlib惰性设置为显式并提取到Sequence<T>接口,以免Iterable默认情况下在所有s上使用它。

  • 对于Iterable<T>上相反,扩展功能的中间操作语义工作急切,处理项目马上和返回另一个Iterable。例如,Iterable<T>.map { ... }返回List<R>包含映射结果的。

    等效的Iterable代码:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    打印输出:

    1 2 before sum
    

    如上所述,Iterable<T>默认情况下它是非惰性的,并且该解决方案很好地展示了自己:在大多数情况下,它具有良好的引用位置,因此可以利用CPU缓存,预测,预取等优势,因此即使对集合进行多次复制也可以正常工作足够,并且在简单的情况下(少量收藏)可以更好地执行。

    如果您需要对评估管道的更多控制,则可以显式转换为具有Iterable<T>.asSequence()功能的惰性序列。


3
对于Java(大多数Guava)粉丝来说,可能是一个很大的惊喜
Venkata Raju

@VenkataRaju对于有经验的人,他们可能会对默认情况下的懒惰感到惊讶。
杰森·米纳德

9
默认情况下,对于较小且更常用的集合,懒惰通常性能较差。如果利用CPU缓存等功能,副本可以比懒惰的副本更快。因此,对于常见用例,最好不要偷懒。不幸的是map,像filter和等函数的通用协定除了从源集合类型中获取以外,没有携带足够的信息来决定其他信息,并且由于大多数集合也是可迭代的,因此这不是“懒惰”的好标记,因为通常在任何地方。懒惰必须明确,以确保安全。
杰森·米纳德

1
@naki来自Apache Spark最近发布的一个示例,他们显然对此感到担忧,请参阅databricks.com/blog/2015/04/28/…的“缓存感知计算”部分, 但他们担心数十亿事情反复进行,所以他们需要走到极端。
杰森·米纳德

3
此外,使用惰性评估的一个常见陷阱是捕获上下文,并将所得的惰性计算与所有捕获的本地对象及其所拥有的内容一起存储在字段中。因此,很难调试内存泄漏。
Ilya Ryzhenkov

49

完成热键的答案:

重要的是要注意Sequence和Iterable如何遍历整个元素:

序列示例:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

记录结果:

filter-Map-Each; 过滤器-地图-每个

可迭代的示例:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

筛选器-筛选器-地图-地图-每个-每个


5
这是两者之间区别的一个很好的例子。
阿列克谢·索辛

这是一个很好的例子。
frye3k

2

Iterable映射到上的java.lang.Iterable接口 JVM,并由常用集合(例如List或Set)实现。对这些扩展的集合扩展功能进行了热切的评估,这意味着它们都将立即处理其输入中的所有元素,并返回包含结果的新集合。

这是一个使用集合函数来获取年龄在21岁以上的列表中前五个人的姓名的简单示例:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

目标平台:JVM在kotlin v。1.3.61上运行。首先,对列表中的每个Person进行年龄检查,并将结果放入全新的列表中。然后,为每个在过滤器运算符之后留下的Person进行名称映射,最后出现在另一个新列表中(现在是List<String>)。最后,创建了最后一个新列表,以包含上一个列表的前五个元素。

相反,Sequence是Kotlin中的一个新概念,代表了惰性评估的值集合。该Sequence接口可以使用相同的集合扩展,但是它们会立即返回Sequence实例,这些实例代表日期的已处理状态,但实际上并未处理任何元素。要开始处理, Sequence必须由终端操作员终止,这基本上是对Sequence的请求,以具体化它具体表示的数据。示例包括toListtoSetsum,仅举几例。调用这些元素时,将仅处理最少数量的元素以产生所需的结果。

将现有集合转换为Sequence非常简单,只需使用asSequence扩展名即可。如上所述,您还需要添加一个终端运算符,否则Sequence将永远不会进行任何处理(再次,懒惰!)。

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

目标平台:在kotlin v。1.3.61上运行的JVMR在这种情况下,将检查Sequence中的Person实例的年龄,如果通过,则提取其名称,然后将其添加到结果列表中。对原始列表中的每个人重复此操作,直到找到五个人。此时,toList函数将返回一个列表,并且Sequence不处理其中的其余人员。

序列还具有其他功能:可以包含无限数量的项目。从这个角度来看,操作员按自己的方式工作是有道理的-无限序列上的操作员如果急于工作,就永远不会返回。

举例来说,下面的序列将生成其终端运算符所需的2的幂(忽略此事实会很快溢出):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

你可以在这里找到更多。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.