如何正确终止 forEach | Kotlin操作符(run、with、let、also、apply)的差异

如何正确终止 forEach

在一次编码中,我发现我的 forEach 函数使用 return@forEach 无法跳出遍历,而是会把所有元素都遍历完,使用 return 的话会直接跳出整个函数体,自然是不符合的,于是我做了些试验,想要得出真正跳出 forEach 遍历的方法

假设我们需要输出一个列表的一部分内容,可以用到 for 循环加个 break 来处理,如下:

1
2
3
4
5
val list = listOf(1, 3, 5, 7, 9)
for (e in list) {
if (e > 3) break
println(e)
}

很简单,当 e 大于 3 时,就会跳出 for 循环,也就是说,会输出 1 和 3 两个数字:

如果改成用 forEach 呢,就会是下面这样:

1
2
3
4
5
val list = listOf(1, 3, 5, 7, 9)
list.forEach {
if (it > 3) ???
println(it)
}

这里的 ??? 应当填写什么呢,我第一反应就写了个 return@forEach,跑起来之后发现,确实达到了我们的效果

那么为什么我在程序中就不行呢?观察之后,我做了下面的变化:

1
2
3
4
5
val list = listOf(1, 3, 5, 7, 9)
list.forEach {
println(it)
if (it > 3) return@forEach
}

这样之后呢,有一些细微的不同,就是会多输出一个 5 而已,因为输出了 5 才执行到 return@forEach 语句,这个也是很简单的

但是跑起来之后就不一样了,居然是下面这样的

原来,上面的例子只是每次大于 3 的时候都跳过了,最多相当于一个 continue,而没有达到 break 的效果,也就是说,在 Lambda 表达式中,return 返回的是所在函数,return@xxx 返回的是 xxx 标签对应的代码块,由于 forEach 后面的这个 Lambda 实际上被调用了多次,因此我们没有办法像 for 循环那样直接 break

实际上我们在 Kotlin 当中用到的 forEach、map、flatMap 等等这样的高阶函数调用,都是流式数据处理的典型例子,重新理一下需求,遇到某一个大于 3 的数,我们就终止遍历,这样的代码用流式 api 写出来应该是这样的:

1
2
3
val list = listOf(1, 3, 5, 7, 9)
list.takeWhile { it <= 3 }.forEach(::println)
println("Hello")

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

但是在 filter 的时候就调用了一次完整的 for-loop,而后面的 forEach 同样再来一遍,也就是说用传统的 for-loop 一遍搞定的事,用流式 api 写了两遍,如果条件比较复杂,出现两遍三遍的情况也是比较正常的,这样就导致了流式 api 性能会比一般 loop 差

除了性能问题,这个实现其实还不是最终的通用的实现,这里只能针对这一个数组做到类似 break 的效果,试想一下,如果我这个数组是无序的,我需要输出数字 3 之前的所有数字,比如:

1
val list = listOf(13, 2, 3, 7, 4)

那肯定不能用上面的写法了,下面这个应该才是最终的解决方案

或者简化为

可能语义不够清晰,一下子比较难看懂,所以也可以用 run 函数写,如下:

Kotlin操作符(run、with、let、also、apply等)的差异

上面提到了几个不同的操作符 run、apply、any、takeWhile,其中 any、takeWhile 是 Kotlin 的 _Collections 扩展中的,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Returns `true` if at least one element matches the given [predicate].
*
* @sample samples.collections.Collections.Aggregates.anyWithPredicate
*/
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return false
for (element in this) if (predicate(element)) return true
return false
}

/**
* Returns a list containing first elements satisfying the given [predicate].
*
* @sample samples.collections.Collections.Transformations.take
*/
public inline fun <T> Iterable<T>.takeWhile(predicate: (T) -> Boolean): List<T> {
val list = ArrayList<T>()
for (item in this) {
if (!predicate(item))
break
list.add(item)
}
return list
}

而 run、apply 等函数则位于 Kotlin 标准库 StandardKt.kt 中,源码位于 kotlin-stdlib-common/kotlin 包下,基本都由内联函数 inline 修饰

  • run

    run 函数在标准库有两个,两个逻辑一样,但是第二个是 T 的扩展函数,入参是一个 block 函数,类型是一个 T 的扩展函数 T.()->R,无参,返回值是 R 类型 ,也可以传入 lambda 表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * Calls the specified function [block] and returns its result.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#run).
    */
    @kotlin.internal.InlineOnly
    public inline fun <R> run(block: () -> R): R {
    // Kotlin 契约的写法,告诉编译器:
    // “这个函数会在此时此处调用‘block’,并且刚好只调用一次”
    // 这里不用管这一段,主要看 return 的部分
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
    }

    /**
    * Calls the specified function [block] with `this` value as its receiver and returns its result.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#run).
    */
    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
    }
  • let

    仔细观察,let 函数和 run 函数的区别在于 T.let 入参传入的 block 函数,其参数是虽然是 T,返回值依旧是 R,block 在内部调用时传入 T.this,调用时用 it 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * Calls the specified function [block] with `this` value as its argument and returns its result.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let).
    */
    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
    }
  • with

    with 函数有两个入参 receiver:T,block 函数,关于 T 的扩展函数,返回 R,return receiver 运行 block 函数后的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#with).
    */
    @kotlin.internal.InlineOnly
    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
    allsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
    }
  • also

    also 函数和 let 函数的区别在于,also 返回的是自身,且入参 block 函数无返回值,和 let 一样,block 在内部调用时传入 T.this,调用时用 it 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * Calls the specified function [block] with `this` value as its argument and returns `this` value.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#also).
    */
    @kotlin.internal.InlineOnly
    @SinceKotlin("1.1")
    public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
    }
  • apply

    apply 函数十分常用,它可以轻松实现 java 的链式调用,在一些比较简单的应用场景就不用很麻烦地写 build 模式了,apply 函数和 also 函数很相似,不同的是,对于 lambda 内部,apply 函数中直接持有 T 的引用,this 可以省略,所以可以直接调用关于 T 的所有 api,而 also 持有的是外部传入的 T 的引用,用 it 表示,所以需要用 it 来调用关于 T 的所有 api

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
    *
    * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply).
    */
    @kotlin.internal.InlineOnly
    public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
    }
  • takeIf、takeUnless

    takeIf 和 takeUnless 只是断言相反,takeIf 也是十分实用的标准函数,传入的 predicate 断言函数,返回值 Boolean,对于 takeIf 而言,符合条件则返回 T,否则返回 null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * Returns `this` value if it satisfies the given [predicate] or `null`, if it doesn't.
    */
    @kotlin.internal.InlineOnly
    @SinceKotlin("1.1")
    public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
    callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
    }

    /**
    * Returns `this` value if it _does not_ satisfy the given [predicate] or `null`, if it does.
    */
    @kotlin.internal.InlineOnly
    @SinceKotlin("1.1")
    public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
    callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
    }
  • repeat

    repeat 标准函数可以轻松实现循环任务,time是循环的次数,action 函数指定具体循环的动作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * Executes the given function [action] specified number of [times].
    *
    * A zero-based index of current iteration is passed as a parameter to [action].
    *
    * @sample samples.misc.ControlFlow.repeat
    */
    @kotlin.internal.InlineOnly
    public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
    action(index)
    }
    }

    用一张流传甚广的图来简单说明,就是如下这样:

参考文章


如何正确终止 forEach | Kotlin操作符(run、with、let、also、apply)的差异
https://enderhoshi.github.io/2019/11/14/如何正确终止 forEach Kotlin 操作符(run、with、let、also、apply)的差异与选择/
作者
HoshIlIlI
发布于
2019年11月14日
许可协议