如何正确终止 forEach

本文来自 Kotlin 中文博客:Kotlinerjavascript

问题背景

话说周六下午团建回来,看到群里的小伙伴们在纠结一个问题,如何退出 forEach 循环,或者说有没有终止 forEach 循环的方法,就像 break 那样。咱们来看个例子:java

val list = listOf(1,3,4,5,6,7,9) 
 for(e in list){ 
     if(e > 3) break 
     println(e) 
 }复制代码

若是 e 大于 3,那么循环终止,这是传统的写法,那么问题来了,咱们如今追求更现代化的写法,要把代码改为 forEach 的函数调用,那么咱们会怎么写呢?算法

list.forEach { 
     if(it > 3) ??? 
     println(it) 
 }复制代码

就像上面这样吗?感受应该是这样,不过大于 3 的时候,究竟该怎么办才能退出这个循环呢?api

return 仍是 return@forEach ?

做为基本上等价于匿名函数的 Lambda 表达式,咱们可能但愿它可以提早返回,这样是否是就至关于终止循环了呢?函数

fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.forEach { 
         if(it > 3) return 
         println(it) 
     } 
 }复制代码

这时候咱们坚决果断的写下了这样的代码,大于 3 的时候,直接 return,结果呢?运行到 4 的时候,forEach 就真的被终止了,后面也就没有了输出。oop

嗯,这样是否是就算把问题解决啦?想一想也不可能呀,否则我这周的文章岂不是太坑人了?性能

fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.forEach { 
         if(it > 3) return 
         println(it) 
     } 
     println("Hello") 
 }复制代码

当咱们把代码改为这样的时候,咱们运行时发现只输出 1 3,后面的 Hello 则是没法打印的。缘由呢,固然也很简单,在 return 眼里,Lambda 表达式都不算事儿,因此咱们在大于 3 时的 return,其实是返回了 main 函数,因而 list.forEach 这个结构以后的代码就不能被执行了。ui

好吧,那这里用 return 确定是有问题的,咱们不用它了行了吧。那不用 return 用什么呢?好在 Kotlin 为咱们提供了标签式的返回方法,也就是说,若是你想从一个 Lambda 表达式当中显式地返回,那么你只须要写 return@xxx 便可,例如:this

fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.forEach { 
         if(it > 3) return@forEach 
         println(it) 
     } 
     println("Hello") 
 }复制代码

你也能够给这个 Lambda 表达式起个新标签名称,好比 block:spa

fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.forEach block@{ 
         if(it > 3) return@block 
         println(it) 
     } 
     println("Hello") 
 }复制代码

这样,咱们的程序运行结果就是:

1 
 3 
 Hello复制代码

这一步你们都会想到的,不过这并非最终的解。

调用仍是循环?

我来问你们一个问题,前面的 forEach 后面传入的 Lambda 表达式体是循环体吗?

固然不是。那其实就是一个函数体,所以对这个函数体的退出只能退出当前的调用。为了说明这个问题,咱们仍是须要对原有的例子作下小修改:

fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.forEach block@{ 
         println("it=$it") 
         if(it > 3) return@block 
         println(it) 
     } 
     println("Hello") 
 }复制代码

结果呢?

it=1 
 1 
 it=3 
 3 
 it=4 
 it=5 
 it=6 
 it=7 
 it=9 
 Hello复制代码

好家伙,尽管咱们在大于 3 的时候 return@block,但看上去仍然没有什么软用,显然,后面的循环仍然执行了。

简单总结一下,在 Lambda 表达式中,return 返回的是所在函数,return@xxx 返回的是 xxx 标签对应的代码块。因为 forEach 后面的这个 Lambda 实际上被调用了屡次,所以咱们没有办法像 for 循环那样直接 break 。

额。。这可如何是好?

流式数据处理

实际上咱们在 Kotlin 当中用到的 forEach、map、flatMap 等等这样的高阶函数调用,都是流式数据处理的典型例子,咱们也看到不甘落后却又跟不上节奏的 Java 在 8.0 推出了 stream Api,其实也无非就是为流式数据处理提供了方便。

采用流式 api 处理数据,咱们就不能再像之前那样思考问题啦,之前的思惟方式多单薄呀,只要是遍历,那就是 for 循环,只要是条件那就是 if...else,却不知世界在变,api 也在变,你不跟着变,那你就请便啦。

那么,回到咱们最开始的问题,需求其实很明确,遇到某一个大于 3 的数,咱们就终止遍历,这样的代码用流式 api 写出来应该是这样的:

val list = listOf(1,3,4,5,6,7,9) 
 list.takeWhile { it <= 3 }.forEach(::println) 
 println("Hello")复制代码

咱们首先经过 takeWhile 来筛选出前面连续不大于 3 的元素,也就是说一旦遇到一个大于 3 的元素咱们就丢弃从这个开始全部后面的元素;接着,咱们把取到的这些不大于 3 的元素经过 forEach 打印出来,这样的话,程序的效果与文章最开头的 for 循环 break 的实现就彻底一致了。

val list = listOf(1,3,4,5,6,7,9) 
 for(e in list){ 
     if(e > 3) break 
     println(e) 
 }复制代码

固然,你可能会说若是我想要打印其中的偶数,那我该怎么写呢?这时候我告诉你们,若是你写出了下面这样的代码,那么我只能告诉你,。。额,我刚想说啥来着??

list.forEach {  
     if(it % 2 == 0){ 
         println(it) 
     } 
 }复制代码

上面这样写的代码呢,让我想起了辫帅张勋:张将军,你知不知道,咱大清已经亡了呢?

list.filter { it % 2 == 0 }.forEach(::println)复制代码

哈哈,若是真的但愿使用流式 api,那么上面这样的写法才算是符合风格的写法。固然了,若是你愿意,你还能够定义一个 isEven 的方法,代码写出来就像下面这样:

fun Int.isEven() = this % 2 == 0 
   
 fun main(args: Array<String>) { 
     val list = listOf(1,3,4,5,6,7,9) 
     list.filter(Int::isEven).forEach(::println) 
 }复制代码

性能

前不久看到有一篇文章对 Java 8 的流式 api 作了评测,说流式 api 的执行效率比传统的 for-loop 差出一倍甚至更多,因此建议你们慎重考虑选择。

其实对于这个东西我认为咱们不必把神经绷这么紧。缘由也很简单呀,流式 api 的执行效率从实现上来说,确实很难达到纯 for-loop 那样的高效,例如咱们前面的:

list.filter(Int::isEvent).forEach(::println)复制代码

在 filter 的时候就调用了一次完整的 for-loop,然后面的 forEach 一样再来一遍,也就是说咱们用传统的 for-loop 一遍搞定的事儿,用流式 api 写了两遍,若是条件比较复杂,出现两遍三遍的状况也是比较正常的。

不过这并不能说明流式 api 就必定要慎重使用。流式 api 更适用于数据的流式处理,特别是涉及到较多 UI 交互的场景,这样的业务逻辑用流式 api 表达起来会很是的简洁直观,也易于维护,相应的,这样的场景对于性能的要求并无到吹毛求疵的地步;而对于性能比较敏感的程序,一般来讲也没有很复杂的业务逻辑,流式 api 在这里也难以发挥做用。

另外,仅仅多个几回循环也并不会改变算法自己的运算效率的数量级,因此对于适用于流式 api 的场景,你们仍是能够放心去使用的。

--

若是你有兴趣加入咱们,请直接关注公众号 Kotlin ,或者加 QQ 群:162452394 联系咱们。