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

Posted

技术标签:

【中文标题】Kotlin 的 Iterable 和 Sequence 看起来完全一样。为啥需要两种类型?【英文标题】:Kotlin's Iterable and Sequence look exactly same. Why are two types required?Kotlin 的 Iterable 和 Sequence 看起来完全一样。为什么需要两种类型? 【发布时间】:2016-06-08 08:05:19 【问题描述】:

这两个接口都只定义了一个方法

public operator fun iterator(): Iterator<T>

文档说Sequence 是懒惰的。但是Iterable 不是也很懒惰吗(除非得到Collection 的支持)?

【问题讨论】:

【参考方案1】:

主要区别在于Iterable&lt;T&gt;Sequence&lt;T&gt; 的语义和stdlib 扩展函数的实现。

对于Sequence&lt;T&gt;,扩展函数尽可能延迟执行,类似于Java Streams 中间 操作。例如,Sequence&lt;T&gt;.map ... 返回另一个 Sequence&lt;R&gt;,并且在调用像 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&lt;T&gt; 用于在您希望尽可能减少 终端 操作中完成的工作时的惰性使用和高效流水线,与 Java Streams 相同。然而,惰性引入了一些开销,这对于较小集合的常见简单转换来说是不可取的,并且会降低它们的性能。

一般来说,没有很好的方法来确定何时需要它,因此在 Kotlin 标准库中将惰性显式化并提取到 Sequence&lt;T&gt; 接口,以避免默认情况下在所有 Iterables 上使用它。

对于Iterable&lt;T&gt;,相反,具有中间操作语义的扩展函数急切地工作,立即处理项目并返回另一个Iterable。例如,Iterable&lt;T&gt;.map ... 返回一个 List&lt;R&gt;,其中包含映射结果。

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&lt;T&gt; 默认情况下是非惰性的,这个解决方案很好地展示了自己:在大多数情况下它具有良好的locality of reference 从而利用 CPU 缓存、预测、预取等,因此甚至可以多次复制的集合仍然足够好,并且在具有小集合的简单情况下表现更好。

如果您需要对评估管道进行更多控制,可以使用 Iterable&lt;T&gt;.asSequence() 函数显式转换为惰性序列。

【讨论】:

对于Java(主要是Guava)的粉丝来说可能是个大惊喜 @VenkataRaju 对于功能性人士来说,他们可能会对默认情况下懒惰的替代品感到惊讶。 Lazy 默认情况下对于更小和更常用的集合通常性能较差。如果利用 CPU 缓存等优势,则副本可能比惰性 eval 更快。所以对于常见的用例最好不要偷懒。不幸的是,像mapfilter 和其他函数的常见合约没有携带足够的信息来决定除了源集合类型之外,并且由于大多数集合也是可迭代的,这不是一个很好的标记“懒惰”,因为它通常无处不在。为了安全,惰性必须是显式的。 @naki 最近 Apache Spark 公告中的一个例子,他们显然担心这一点,请参阅databricks.com/blog/2015/04/28/… 的“缓存感知计算”部分......但他们担心数十亿的事情迭代所以他们需要走极端。 此外,惰性求值的一个常见缺陷是捕获上下文并将产生的惰性计算与所有捕获的本地变量以及它们所持有的任何内容一起存储在一个字段中。因此,很难调试内存泄漏。【参考方案2】:

完成热键的回答:

请务必注意 Sequence 和 Iterable 如何在您的元素中进行迭代:

序列示例:

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

日志结果:

过滤器 - 地图 - 每个;过滤器 - 地图 - 每个

可迭代示例:

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

过滤器-过滤器-地图-地图-每个-每个

【讨论】:

这是两者之间区别的一个很好的例子。 这是一个很好的例子。【参考方案3】:

Iterable 映射到java.lang.Iterable 接口上 JVM,由常用的集合实现,如 List 或 放。评估这些上的集合扩展功能 急切地,这意味着他们都立即处理所有元素 他们的输入并返回一个包含结果的新集合。

这是一个使用集合函数获取 列表中年龄至少为 21 岁的前五个人的姓名:

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

目标平台:JVMRunning on kotlin v. 1.3.61 一、年龄检查 对列表中的每个人都完成,结果放在一个 全新的名单。然后,为每个人完成到他们名字的映射 在过滤器操作员之后留下的人,最终在 另一个新列表(现在是List&lt;String&gt;)。最后,有一个 创建的最后一个新列表以包含 上一个列表。

相比之下,Sequence 是 Kotlin 中的一个新概念,用来表示一个懒惰的 评估值的集合。相同的集合扩展是 可用于Sequence 接口,但这些会立即返回 表示日期的已处理状态的序列实例,但 没有实际处理任何元素。要开始处理, Sequence 必须由终端操作员终止,这些是 基本上是对序列的请求以实现它的数据 以某种具体形式表示。示例包括toListtoSet、 和sum,仅举几例。当这些被调用时,只有 将处理最少所需数量的元素以生成 要求的结果。

将现有集合转换为序列很漂亮 直截了当,您只需要使用 asSequence 扩展名。作为 上面提到的,还需要添加终端操作符,否则 序列永远不会做任何处理(再次,懒惰!)。

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

目标平台:JVMRunning on kotlin v. 1.3.61 在这种情况下, 序列中的每个人实例都会检查他们的年龄,如果 他们通过了,他们的名字被提取出来,然后添加到 结果列表。对原始列表中的每个人重复此操作 直到找到五个人。此时,toList函数 返回一个列表,而Sequence 中的其余人则不是 已处理。

序列还有一些额外的能力:它可以包含 无限数量的项目。从这个角度来看,这是有道理的 运营商按照他们的方式工作 - 一个无限的运营商 如果它急于完成它的工作,序列将永远不会返回。

作为一个例子,这里有一个序列会产生尽可能多的 2 根据其终端运营商的要求(忽略这一事实 会很快溢出):

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

您可以找到更多here。

【讨论】:

【参考方案4】:

Iterable 对于大多数用例来说已经足够好了,由于空间局部性,在它们上执行迭代的方式非常适用于缓存。 但他们的问题是整个集合必须通过第一个中间操作才能移动到第二个等等。

sequence中,每个项目在处理下一个项目之前都经过完整管道.

此属性可能会影响代码的性能,尤其是在迭代大型数据集时。因此,如果您的终端操作很可能提前终止,那么sequence 应该是首选,因为您可以通过不执行不必要的操作来节省开支。例如

sequence.filter  getFilterPredicate()    
        .map     getTransformation()   
        .first   getSelector() 

在上述情况下,如果第一项满足filter 谓词,并且在map 转换满足选择条件后,则filtermapfirst 仅被调用一次。

如果是可迭代的整个集合,必须首先过滤然后映射,然后开始第一次选择

【讨论】:

以上是关于Kotlin 的 Iterable 和 Sequence 看起来完全一样。为啥需要两种类型?的主要内容,如果未能解决你的问题,请参考以下文章

kotlin中的Sequences(序列)与Iterable(集合)区别

为啥 Kotlin Array<T> 不实现 Iterable<T>

kotlin修炼指南9-Sequence的秘密

kotlin修炼指南9-Sequence的秘密

快速上手 Kotlin 开发系列之操作符

Storyboard & Seque:展示的 VC 如何知道展示的 VC 啥时候解散自己?