5.5.1 条件与循环

原文链接: https://kotlinlang.org/docs/control-flow.html

5.5.1 条件与循环

Kotlin 为你提供了灵活的工具来控制程序流程。使用 if、when 和循环,可以为你的条件定义清晰且富有表现力的逻辑。

If 表达式

在 Kotlin 中使用 if 时,把要检查的条件写在圆括号 () 中,把结果为真时要执行的操作写在花括号 {} 中。你可以用 else 和 else if 添加更多分支和检查。

你也可以把 if 写成表达式,这样就能把它的返回值直接赋给变量。在这种形式下,else 分支是必需的。if 表达式的作用与其他语言中的三元运算符(condition ? then : else)相同。

例如:

 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
fun main() {
    val heightAlice = 160
    val heightBob = 175

    var taller = heightAlice
    if (heightAlice < heightBob) taller = heightBob

    // 使用 else 分支
    if (heightAlice > heightBob) {
        taller = heightAlice
    } else {
        taller = heightBob
    }

    // 把 if 用作表达式
    taller = if (heightAlice > heightBob) heightAlice else heightBob

    // 把 else if 用作表达式:
    val heightLimit = 150
    val heightOrLimit = if (heightLimit > heightAlice) heightLimit else if (heightAlice > heightBob) heightAlice else heightBob

    println("Taller height is $taller")
    // 较高的身高是 175
    println("Height or limit is $heightOrLimit")
    // 身高或上限是 175
}

if 表达式中的每个分支都可以是一个代码块,此时最后一个表达式的值就是该分支的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main() {
    val heightAlice = 160
    val heightBob = 175

    val taller = if (heightAlice > heightBob) {
        print("Choose Alice\n")
        heightAlice
    } else {
        print("Choose Bob\n")
        heightBob
    }

    println("Taller height is $taller")
}

when 表达式与语句

when 是一种条件表达式,它会根据多个可能的值或条件来执行代码。它类似于 Java、C 等语言中的 switch 语句。when 会计算其参数,并按顺序与每个分支比较,直到某个分支条件得到满足。例如:

1
2
3
4
5
6
7
8
9
fun main() {
    val userRole = "Editor"
    when (userRole) {
        "Viewer" -> print("User has read-only access")
        "Editor" -> print("User can edit content")
        else -> print("User role is not recognized")
    }
    // 用户可以编辑内容
}

when 既可以作为表达式使用,也可以作为语句使用。作为表达式时,when 会返回一个值,供你在后续代码中使用;作为语句时,when 只完成一个操作,不返回结果:

表达式语句
kotlin // 返回一个字符串并赋给 // text 变量 val text = when (x) { 1 -> "x == 1" 2 -> "x == 2" else -> "x is neither 1 nor 2" }kotlin // 不返回结果,只触发一条 // 打印语句 when (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> print("x is neither 1 nor 2") }

其次,when 可以带主语(subject),也可以不带。两种情况的行为相同。使用主语通常会让代码更易读、更易维护,因为它清楚地表明了你在检查什么。

带主语 x不带主语
kotlin when(x) { ... }kotlin when { ... }

你如何使用 when 决定了分支中是否需要覆盖所有可能的情况。覆盖所有可能情况称为穷尽(exhaustive)。

语句

如果把 when 当作语句使用,就不需要覆盖所有可能的情况。在下面这个例子中,有些情况没有被覆盖,因此不会触发任何分支,但也不会产生错误:

1
2
3
4
5
6
7
8
fun main() {
    val deliveryStatus = "OutForDelivery"
    when (deliveryStatus) {
        // 并非所有情况都被覆盖
        "Pending" -> print("Your order is being prepared")
        "Shipped" -> print("Your order is on the way")
    }
}

与 if 一样,每个分支都可以是一个代码块,其值就是代码块中最后一个表达式的值。

表达式

如果把 when 当作表达式使用,就必须覆盖所有可能的情况。第一个匹配分支的值就是整个表达式的值。如果没有覆盖所有情况,编译器会报错。

如果你的 when 表达式带有主语,可以用 else 分支来确保覆盖所有可能的情况,但这不是必需的。例如,当主语是 Boolean、enum 类、sealed 类或它们的可空形式时,可以不用 else 分支就覆盖所有情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import kotlin.random.Random
enum class Bit {
    ZERO, ONE
}

fun getRandomBit(): Bit {
    return if (Random.nextBoolean()) Bit.ONE else Bit.ZERO
}

fun main() {
    val numericValue = when (getRandomBit()) {
        // 不需要 else 分支,因为所有情况都已覆盖
        Bit.ZERO -> 0
        Bit.ONE -> 1
    }

    println("Random bit as number: $numericValue")
    // 随机比特值:0
}

提示: 要简化 when 表达式并减少重复,可以试试上下文敏感解析(目前为预览功能)。当预期类型已知时,该功能允许你在 when 表达式中省略枚举项或密封类成员的类型名。更多信息请参阅上下文敏感解析预览或相关的 KEEP 提案。

如果你的 when 表达式没有主语,就必须有 else 分支,否则编译器会报错。当其他分支条件都不满足时,会执行 else 分支:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun main() {
    val localFileSize = 1200
    val remoteFileSize = 1200

    val message = when {
        localFileSize > remoteFileSize -> "Local file is larger than remote file"
        localFileSize < remoteFileSize -> "Local file is smaller than remote file"
        else -> "Local and remote files are the same size"
    }

    println(message)
    // 本地文件和远程文件大小相同
}

when 的其他用法

when 表达式和语句提供了多种方式来简化代码、处理多个条件并执行类型检查。

用逗号可以把多个条件归入同一个分支:

1
2
3
4
5
6
7
fun main() {
    val ticketPriority = "High"
    when (ticketPriority) {
        "Low", "Medium" -> print("Standard response time")
        else -> print("High-priority handling")
    }
}

把求值结果为 true 或 false 的表达式用作分支条件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    val storedPin = "1234"
    val enteredPin = 1234

    when (enteredPin) {
        // 表达式
        storedPin.toInt() -> print("PIN is correct")
        else -> print("Incorrect PIN")
    }
}

使用 in 或 !in 关键字检查某个值是否在区间或集合中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    val x = 7
    val validNumbers = setOf(15, 16, 17)

    when (x) {
        in 1..10 -> print("x is in the range")
        in validNumbers -> print("x is valid")
        !in 10..20 -> print("x is outside the range")
        else -> print("none of the above")
    }
}

使用 is 或 !is 关键字检查值的类型。由于有智能转换,你可以直接访问该类型的成员函数和属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun hasPrefix(input: Any): Boolean = when (input) {
    is String -> input.startsWith("ID-")
    else -> false
}

fun main() {
    val testInput = "ID-98345"
    println(hasPrefix(testInput))
    // true
}

可以用 when 代替传统的 if-else if 链。不带主语时,分支条件就是布尔表达式,第一个条件为 true 的分支会被执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun Int.isOdd() = this % 2 != 0
fun Int.isEven() = this % 2 == 0

fun main() {
    val x = 5
    val y = 8

    when {
        x.isOdd() -> print("x is odd")
        y.isEven() -> print("y is even")
        else -> print("x+y is odd")
    }
    // x 是奇数
}

最后,可以用下面的语法把主语捕获到变量中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    val message = when (val input = "yes") {
        "yes" -> "You said yes"
        "no" -> "You said no"
        else -> "Unrecognized input: $input"
    }

    println(message)
    // 你说了 yes
}

作为主语引入的变量,其作用域仅限于该 when 表达式或语句的主体。

守卫条件

守卫条件(guard condition)允许你为 when 表达式或语句的分支指定多个条件,从而让复杂的控制流更明确、更简洁。只要 when 带有主语,就可以使用守卫条件。

把守卫条件放在同一分支中主条件之后,用 if 分隔:

 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
29
30
31
32
33
sealed interface Animal {
    data class Cat(val mouseHunter: Boolean) : Animal
    data class Dog(val breed: String) : Animal
}

fun feedDog() = println("Feeding a dog")
fun feedCat() = println("Feeding a cat")

fun feedAnimal(animal: Animal) {
    when (animal) {
        // 只有主条件的分支
        // 当 animal 是 Dog 时调用 feedDog()
        is Animal.Dog -> feedDog()
        // 同时包含主条件和守卫条件的分支
        // 当 animal 是 Cat 且不是 mouseHunter 时调用 feedCat()
        is Animal.Cat if !animal.mouseHunter -> feedCat()
        // 如果以上条件都不匹配,则打印 "Unknown animal"
        else -> println("Unknown animal")
    }
}

fun main() {
    val animals = listOf(
        Animal.Dog("Beagle"),
        Animal.Cat(mouseHunter = false),
        Animal.Cat(mouseHunter = true)
    )

    animals.forEach { feedAnimal(it) }
    // 正在喂狗
    // 正在喂猫
    // 未知的动物
}

当存在用逗号分隔的多个条件时,不能使用守卫条件。例如:

1
0, 1 -> print("x == 0 or x == 1")

在同一个 when 表达式或语句中,你可以混用带守卫条件和不带守卫条件的分支。带守卫条件的分支只有在主条件和守卫条件都为 true 时才会执行。如果主条件不匹配,守卫条件不会被求值。

由于 when 语句不需要覆盖所有情况,所以在没有 else 分支的 when 语句中使用守卫条件意味着:如果没有任何条件匹配,就不会执行任何代码。

与语句不同,when 表达式必须覆盖所有情况。如果在没有 else 分支的 when 表达式中使用守卫条件,编译器会要求你处理所有可能的情况,以避免运行时错误。

可以使用布尔运算符 &&(与)或 ||(或)在单个分支中组合多个守卫条件。请用圆括号把布尔表达式括起来,以避免歧义:

1
2
3
when (animal) {
    is Animal.Cat if (!animal.mouseHunter && animal.hungry) -> feedCat()
}

守卫条件也支持 else if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
when (animal) {
    // 检查 `animal` 是否为 `Dog`
    is Animal.Dog -> feedDog()
    // 守卫条件:检查 `animal` 是否为 `Cat` 且不是 `mouseHunter`
    is Animal.Cat if !animal.mouseHunter -> feedCat()
    // 如果以上条件都不匹配且 animal.eatsPlants 为 true,则调用 giveLettuce()
    else if animal.eatsPlants -> giveLettuce()
    // 如果以上条件都不匹配,则打印 "Unknown animal"
    else -> println("Unknown animal")
}

JVM 上的字节码生成

当你为 JVM 21 或更高版本编译 Kotlin 代码时,编译器会为符合条件的 when 表达式生成 invokedynamic 指令。这会生成更小的字节码,与 Java switch 语句生成的字节码类似。

当满足以下所有条件时,编译器会使用 invokedynamic 以及 SwitchBootstraps.typeSwitch() 方法:

  • 除 else 之外的所有条件都是 is 或 null 检查。
  • when 表达式不包含守卫条件(if)。
  • 条件中不包含无法直接进行类型检查的类型,例如可变 Kotlin 集合(MutableList)或函数类型(kotlin.Function1、kotlin.Function2 等)。
  • when 表达式除 else 之外至少还有两个条件。
  • 所有分支检查的都是 when 表达式的同一个主语。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
open class Shape

class Circle : Shape()
class Rectangle : Shape()
class Triangle : Shape()

fun countCorners(shape: Shape) = when (shape) {
    is Circle -> 0
    is Rectangle -> 4
    is Triangle -> 3
    else -> -1
}

这里的 when (shape) 表达式在字节码中会编译成单个 invokedynamic 类型 switch,而不是多次 instanceof 检查。

for 循环

使用 for 循环遍历集合、数组或区间:

1
for (item in collection) print(item)

for 循环的主体可以是带花括号 {} 的代码块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    val shoppingList = listOf("Milk", "Bananas", "Bread")
    println("Things to buy:")
    for (item in shoppingList) {
        println("- $item")
    }
    // 要买的东西:
    // - 牛奶
    // - 香蕉
    // - 面包
}

区间

要遍历一个数字区间,请使用带 .. 和 ..< 运算符的区间表达式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() {
    println("Closed-ended range:")
    for (i in 1..6) {
        print(i)
    }
    // 闭区间:
    // 123456

    println("\nOpen-ended range:")
    for (i in 1..<6) {
        print(i)
    }
    // 开区间:
    // 12345

    println("\nReverse order in steps of 2:")
    for (i in 6 downTo 0 step 2) {
        print(i)
    }
    // 以步长 2 逆序:
    // 6420
}

数组

如果你想带索引遍历数组或列表,可以使用 indices 属性:

1
2
3
4
5
6
7
8
9
fun main() {
    val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee")
    for (i in routineSteps.indices) {
        println(routineSteps[i])
    }
    // 起床
    // 刷牙
    // 煮咖啡
}

或者,你可以使用标准库中的 .withIndex() 函数:

1
2
3
4
5
6
7
8
9
fun main() {
    val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee")
    for ((index, value) in routineSteps.withIndex()) {
        println("The step at $index is \"$value\"")
    }
    // 第 0 步是 "Wake up"
    // 第 1 步是 "Brush teeth"
    // 第 2 步是 "Make coffee"
}

迭代器

for 循环可以遍历任何提供迭代器的对象。集合默认提供迭代器,而区间和数组会被编译成基于索引的循环。

你可以通过提供名为 iterator() 的成员函数或扩展函数(返回 Iterator<>)来创建自己的迭代器。iterator() 函数必须有一个 next() 函数和一个返回 Boolean 的 hasNext() 函数。

为类创建自定义迭代器最简单的方式,是继承 Iterable<T> 接口,并重写其中已有的 iterator()、next() 和 hasNext() 函数。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Booklet(val totalPages: Int) : Iterable<Int> {
    override fun iterator(): Iterator<Int> {
        return object : Iterator<Int> {
            var current = 1
            override fun hasNext() = current <= totalPages
            override fun next() = current++
        }
    }
}

fun main() {
    val booklet = Booklet(3)
    for (page in booklet) {
        println("Reading page $page")
    }
    // 正在读第 1 页
    // 正在读第 2 页
    // 正在读第 3 页
}

提示: 进一步了解接口和继承。

你也可以完全从头创建这些函数。这种情况下,需要给函数加上 operator 关键字:

 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
class Booklet(val totalPages: Int) {
    operator fun iterator(): Iterator<Int> {
        return object {
            var current = 1

            operator fun hasNext() = current <= totalPages
            operator fun next() = current++
        }.let {
            object : Iterator<Int> {
                override fun hasNext() = it.hasNext()
                override fun next() = it.next()
            }
        }
    }
}

fun main() {
    val booklet = Booklet(3)
    for (page in booklet) {
        println("Reading page $page")
    }
    // 正在读第 1 页
    // 正在读第 2 页
    // 正在读第 3 页
}

while 循环

while 和 do-while 循环会在条件满足时不断执行循环体中的代码。两者的区别在于检查条件的时机:

  • while 先检查条件,如果满足,就执行循环体中的代码,然后回到条件检查。
  • do-while 先执行循环体中的代码,然后检查条件。如果满足,就重复循环。因此,无论条件如何,do-while 的循环体至少会执行一次。

对于 while 循环,把要检查的条件放在圆括号 () 中,把循环体放在花括号 {} 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun main() {
    var carsInGarage = 0
    val maxCapacity = 3
    while (carsInGarage < maxCapacity) {
        println("Car entered. Cars now in garage: ${++carsInGarage}")
    }
    // 车辆已进入。车库中现有车辆:1
    // 车辆已进入。车库中现有车辆:2
    // 车辆已进入。车库中现有车辆:3

    println("Garage is full!")
    // 车库已满!
}

对于 do-while 循环,先把循环体放在花括号 {} 中,然后再把要检查的条件放在圆括号 () 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import kotlin.random.Random

fun main() {
    var roll: Int
    do {
        roll = Random.nextInt(1, 7)
        println("Rolled a $roll")
    } while (roll != 6)
    // 掷出了 2
    // 掷出了 6

    println("Got a 6! Game over.")
    // 得到 6!游戏结束。
}

循环中的 break 与 continue

Kotlin 在循环中支持传统的 break 和 continue 运算符。请参阅返回与跳转。