Kotlin stdlib操作与循环

我写了下面的代码:

val src = (0 until 1000000).toList() val dest = ArrayList(src.size / 2 + 1) for (i in src) { if (i % 2 == 0) dest.add(Math.sqrt(i.toDouble())) } 

IntellJ(在我的情况下是AndroidStudio)问我是否要用stdlib中的操作替换for循环。 这导致以下代码:

 val src = (0 until 1000000).toList() val dest = ArrayList(src.size / 2 + 1) src.filter { it % 2 == 0 } .mapTo(dest) { Math.sqrt(it.toDouble()) } 

现在我必须说,我喜欢改变的代码。 当我提出类似的情况时,我觉得写起来比循环更容易。 然而在阅读filter函数后,我意识到这是比for循环慢得多的代码。 filter函数创建一个新的列表,其中只包含来自src的匹配谓词的元素。 因此,在stdlib版本的代码中还有一个创建的列表和一个循环。 Ofc对于小列表来说可能并不重要,但总的来说,这听起来不是一个好的选择。 特别是如果你需要链接更多的方法,你可以通过编写一个for循环来避免大量额外的循环。

我的问题是在Kotlin被认为是好的做法。 我应该坚持循环还是我想念的东西,它不工作,因为我认为它的工作原理。

如果你关心性能,你需要的是Sequence 。 例如,你的上面的代码将会是

 val src = (0 until 1000000).toList() val dest = ArrayList(src.size / 2 + 1) src.asSequence() .filter { it % 2 == 0 } .mapTo(dest) { Math.sqrt(it.toDouble()) } 

在上面的代码中, filter返回另一个Sequence ,表示一个中间步骤。 没有什么是真正创建,没有对象或数组创建(除了一个新的Sequence包装)。 只有在mapTo终端操作符mapTomapTo创建结果集合。

如果你已经学过java 8流,你可能会发现上面的解释有点熟悉。 实际上, Sequence大概是java 8 Stream的kotlin等价物。 他们有相似的目的和性能特点。 唯一的区别是Sequence并不是为了与ForkJoinPool一起工作而设计的,因此更容易实现。

当涉及多个步骤或者集合可能很大时,建议使用Sequence而不是plain .filter {...}.mapTo{...} 。 我还建议你使用Sequenceforms,而不是命令forms,因为它更容易理解。 当数据处理涉及5个或更多的步骤时,势在必行的forms可能变得复杂,难以理解。 如果只有一个步骤,你不需要一个Sequence ,因为它只是创建垃圾,并没有什么用处。

你错过了一些东西。 🙂

在这种情况下,您可以使用IntProgression

 val progression = 0 until 1_000_000 step 2 

然后,您可以用各种方式创建您想要的正方形列表:

 // may make the list larger than necessary // its internal array is copied each time the list grows beyond its capacity // code is very straight forward progression.map { Math.sqrt(it.toDouble()) } // will make the list the exact size needed // no copies are made // code is more complicated progression.mapTo(ArrayList(progression.last / 2 + 1)) { Math.sqrt(it.toDouble()) } // will make the list the exact size needed // a single intermediate list is made // code is minimal and makes sense progression.toList().map { Math.sqrt(it.toDouble()) } 

我的建议是选择你喜欢的编码风格。 Kotlin既是面向对象又是function性语言,这意味着你的两个命题都是正确的。

通常情况下,function结构有利于可读性而不是性能; 但是,在某些情况下,程序代码也会更具可读性。 你应该尽可能地坚持一种风格,但是如果你觉得它更适合你的约束,可读性,性能或者两者兼而有之,就不要害怕改变一些代码。

转换后的代码不需要手动创建目的地列表,可以简化为:

 val src = (0 until 1000000).toList() val dest = src.filter { it % 2 == 0 } .map { Math.sqrt(it.toDouble()) } 

正如@ glee8e的优秀回答所提到的,你可以使用一个序列来做一个懒惰的评估。 使用序列的简化代码:

 val src = (0 until 1000000).toList() val dest = src.asSequence() // change to lazy .filter { it % 2 == 0 } .map { Math.sqrt(it.toDouble()) } .toList() // create the final list 

请注意,最后添加的toList()是从一个序列变回到最终的列表,这是在处理过程中所做的一个副本。 您可以省略该步骤以保持顺序。

@hotkey强调这些评论是非常重要的,他们说你不应该总是认为另一个迭代或者一个副本会导致比懒惰的评估更糟的性能。 @hotkey说:

有时几个循环。 即使他们复制整个collections,由于良好的地方性参考表现出色的表现。 请参阅: Kotlin的Iterable和Sequence看起来完全一样。 为什么需要两种types?

摘自该链接:

…在大多数情况下,它具有良好的参考位置,因此可以利用CPU缓存,预测,预取等优势,从而即使集合的多个副本仍然可以工作得很好,并且在小集合的简单情况下可以更好地执行。

@ glee8e表示Kotlin序列和Java 8流之间有相似之处,详细的比较请看: 标准Kotlin库中有哪些Java 8 Stream.collect等价物可用?