在 Kotlin 的函数式循环中,如何执行“break”或“continue”操作?

70
在 Kotlin 中,我无法在函数循环和 lambda 中像在普通的 for 循环中那样使用 break 或 continue。例如,以下代码是不起作用的:
(1..5).forEach {
    continue@forEach  // not allowed, nor break@forEach
}

旧的文档提到了这个功能,但它似乎从未被实现。当我想要在lambda内部使用continuebreak时,有什么最佳方法可以获得相同的行为?

注意:这个问题是作者自己写并回答的(自问自答的问题),以便在SO中包含常见的Kotlin主题的成语化答案。也为了澄清一些针对Kotlin alpha版编写的非当前Kotlin准确答案。


请注意:在使用forEach函数的lambda表达式的最后一行中使用return@forEach时,最好提及IDE(可能是Android Studio或Intellij),以免显示警告。 - David
2022年10月,所有后端的编译器实现已经准备就绪,IDE支持和使用KEEP的文档正在进行中。 - Vadzim
很快就会发布,但尚未确定具体发布日期,可能在1.8.x版本中。 - Jayson Minard
2个回答

135

除了你所要求的功能,还有其他选项可以提供类似的功能。例如:

你可以使用filter来避免处理某些值:(continue一样

dataSet.filter { it % 2 == 0 }.forEach {
    // do work on even numbers
}

你可以使用 takeWhile 来停止一个函数循环:(类似于 break
dataSet.takeWhile { it < 10 }.forEach {
    // do work on numbers as long as they are < 10, otherwise stop
}

一个更加复杂的例子,虽然毫无意义,但你想进行一些处理,跳过一些结果值,然后在一组不同条件下停止,如下所示:

dataSet.asSequence()
       .takeWhile { it >=  0 }    // a -1 signals end of the dataset (break)
       .map { it + 1 }            // increment each number
       .filterNot { it % 5 == 0 } // skip (continue) numbers divisible by 5
       .map { it - 1 }            // decrement each number by 1
       .filter { it < 100 }       // skip (continue) if number is >= 100
       .drop(5)                   // ignore the first 5 numbers
       .take(10)                  // use the next 10 numbers and end
       .forEach {
           // do work on the final list
       }

这些函数的组合往往可以消除对continuebreak的需求。而且这里有无数不同的选项,超出了可以记录的范围。为了了解可以做什么,最好学习Kotlin标准库中所有可用的函数collections, 惰性sequences, 和iterable
有时候,你会遇到一些突变的状态需要使用breakcontinue,这在函数模型下很难实现。你可以使用更复杂的函数,如foldreducefiltertakeWhile函数相结合,但有时这更难理解。因此,如果你真的想要那种确切的行为,你可以使用return from lambda expression来模拟continuebreak>,具体取决于你的用法。
以下是一个模拟continue的示例:
(1..5).forEach  {
    if (it == 3) return@forEach  // mimic continue@forEach
    // ... do something more
}

您可以更加复杂,使用标签来处理嵌套或混乱的情况:

(1..3).forEach outer@ { x ->
    (1..3).forEach inner@ { y ->
        if (x == 2 && y == 2) return@outer // mimic continue@outer
        if (x == 1 && y == 1) return@inner // mimic continue@inner
        // ... do something more
    }
}

如果你想要执行一个 break,你需要在循环外使用一个可以从中返回的东西,这里我们将使用 run() 函数来帮助我们:

run breaker@ {
    (1..20).forEach { x ->
        if (x == 5) return@breaker  // mimic break@forEach
        // ... do something more
    }
}

与其使用run(),还可以使用let()apply(),或者任何你在forEach周围自然使用的内容作为你想要中断的位置。但是你也将跳过forEach后面同一块中的代码,所以要小心。

这些是内联函数,因此它们实际上并不会增加额外开销。

阅读Kotlin参考文档返回和跳转,了解所有特殊情况,包括匿名函数。


这里是一个单元测试,证明所有的工作都正常:

@Test fun testSo32540947() {
    val results = arrayListOf<Pair<Int,Int>>()
    (1..3).forEach outer@ { x ->
        (1..3).forEach inner@ { y ->
            if (x == 2 && y == 2) return@outer // continue @outer
            if (x == 1 && y == 1) return@inner // continue @inner
            results.add(Pair(x,y))
        }
    }

    assertEquals(listOf(Pair(1,2), Pair(1,3), Pair(2,1), Pair(3,1), Pair(3,2), Pair(3,3)), results)

    val results2 = arrayListOf<Int>()
    run breaker@ {
        (1..20).forEach { x ->
            if (x == 5) return@breaker
            results2.add(x)
        }
    }

    assertEquals(listOf(1,2,3,4), results2)
}

在代码中使用带标签的break和return语句是一种“代码异味”(依我之见)。请参考下面@user8320224的答案,那里提供了更加优雅和惯用的函数式解决方案。 - Mark
2
@Mark 我调整了答案,将功能模型和字面意思的答案结合起来。 - Jayson Minard
1
@store88 扩展了答案以提供更多选项。 - Jayson Minard
1
不幸的是,过滤器会给我一个新列表,所以如果我需要改变一些东西,我就不能使用它。而且@breaker需要在外部标记一些东西,所以这取决于具体情况。 - Yava
forEachIndexed? - Crag

2

takeWhile标准库函数可以替代break。

例如,

val array = arrayOf(2, 8, 4, 5, 13, 12, 16)
array.takeWhile { it % 2 == 0 }.forEach { println(it) } // break on odd
array.takeWhile { it % 3 != 0 }.forEach { println(it) } // break on 3 * n

一个使用示例会提升你的回答。 - Martin Evans
3
请注意,这将把所有满足条件的元素复制到新分配的中间集合中。 - Vadzim
当处理序列而不是数组时,上述关于中间集合的担忧是无关紧要的(因为序列是惰性的,不会构建中间集合)。在我看来,这比被接受的答案更好,你只需要在序列上使用它。 - Mark
序列并不总是更快,这取决于列表的大小和所采取的操作。这已经被反复证明过了。 - Jayson Minard
@JaysonMinard 如果你的评论是针对我说的(看起来是这样),请注意我并没有说序列会更快 - 只是使用序列会使之前提到的中间集合问题无效。 - Mark
这只是一个通用的想法,以确保不会被误解。 - Jayson Minard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接