如何正确终止 forEach | Kotlin操作符(run、with、let、also、apply)的差异
如何正确终止 forEach
在一次编码中,我发现我的 forEach 函数使用 return@forEach 无法跳出遍历,而是会把所有元素都遍历完,使用 return 的话会直接跳出整个函数体,自然是不符合的,于是我做了些试验,想要得出真正跳出 forEach 遍历的方法
假设我们需要输出一个列表的一部分内容,可以用到 for 循环加个 break 来处理,如下:
1 |
|
很简单,当 e 大于 3 时,就会跳出 for 循环,也就是说,会输出 1 和 3 两个数字:
如果改成用 forEach 呢,就会是下面这样:
1 |
|
这里的 ??? 应当填写什么呢,我第一反应就写了个 return@forEach,跑起来之后发现,确实达到了我们的效果
那么为什么我在程序中就不行呢?观察之后,我做了下面的变化:
1 |
|
这样之后呢,有一些细微的不同,就是会多输出一个 5 而已,因为输出了 5 才执行到 return@forEach 语句,这个也是很简单的
但是跑起来之后就不一样了,居然是下面这样的
原来,上面的例子只是每次大于 3 的时候都跳过了,最多相当于一个 continue,而没有达到 break 的效果,也就是说,在 Lambda 表达式中,return 返回的是所在函数,return@xxx 返回的是 xxx 标签对应的代码块,由于 forEach 后面的这个 Lambda 实际上被调用了多次,因此我们没有办法像 for 循环那样直接 break
实际上我们在 Kotlin 当中用到的 forEach、map、flatMap 等等这样的高阶函数调用,都是流式数据处理的典型例子,重新理一下需求,遇到某一个大于 3 的数,我们就终止遍历,这样的代码用流式 api 写出来应该是这样的:
1 |
|
首先通过 takeWhile 来筛选出前面连续不大于 3 的元素,也就是说一旦遇到一个大于 3 的元素我们就丢弃从这个开始所有后面的元素;接着,我们把取到的这些不大于 3 的元素通过 forEach 打印出来,这样的话,程序的效果与最开头的 for 循环 break 的实现就完全一致了
但是在 filter 的时候就调用了一次完整的 for-loop,而后面的 forEach 同样再来一遍,也就是说用传统的 for-loop 一遍搞定的事,用流式 api 写了两遍,如果条件比较复杂,出现两遍三遍的情况也是比较正常的,这样就导致了流式 api 性能会比一般 loop 差
除了性能问题,这个实现其实还不是最终的通用的实现,这里只能针对这一个数组做到类似 break 的效果,试想一下,如果我这个数组是无序的,我需要输出数字 3 之前的所有数字,比如:
1 |
|
那肯定不能用上面的写法了,下面这个应该才是最终的解决方案
或者简化为
可能语义不够清晰,一下子比较难看懂,所以也可以用 run 函数写,如下:
Kotlin操作符(run、with、let、also、apply等)的差异
上面提到了几个不同的操作符 run、apply、any、takeWhile,其中 any、takeWhile 是 Kotlin 的 _Collections 扩展中的,源码如下:
1 |
|
而 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)
}
}用一张流传甚广的图来简单说明,就是如下这样: