昨天已经把变量、类型、函数和字符串模板过了一遍,它们解决的是"数据怎么表示、逻辑怎么封装、结果怎么输出"的问题。但程序真正开始变得有用,往往是从控制流程开始的。用户输入不同,程序要给出不同反馈,数据不止一条,程序要重复处理,某个条件还没满足,程序就要继续等待或重试。这些都不是单靠变量声明能完成的。
今天要讲的 if、when、for 和 while,就是 Kotlin 里最基础也最常用的控制流工具。它们看起来只是几个关键字,但背后对应的是四种很典型的思维方式,判断一个条件、在多个分支里选择一个、遍历一组数据、在条件满足前持续执行。把这几种写法掌握住,后面写 Android 页面逻辑、处理列表数据、校验用户输入时,代码就不会只停留在"顺序执行"的阶段。
if:不只是一个语句
if 在 Kotlin 里仍然是最直接的条件判断工具。它接收一个布尔条件,条件为 true 时执行对应分支,条件不满足时可以交给 else。如果你以前写过 Java、JavaScript 或 C,这部分语法并不陌生,真正需要注意的是,Kotlin 里的 if 不只是"执行一段代码"的语句,它还可以作为表达式返回一个值。
这个设计会影响你写代码的方式。在 Java 里经常用三元运算符 condition ? a : b 给变量赋值,而 Kotlin 没有三元运算符,因为 if 表达式已经覆盖了这个场景。条件判断和赋值可以合在一起写,代码会更集中,也更容易看出"这个变量由哪几个分支决定"。
kotlin
val score = 85
val result = if (score >= 60) "及格" else "不及格"
println(result) // 及格
上面这段代码里,if 会根据 score >= 60 的结果在两个字符串里选一个,最终 result 拿到的是 "及格"。这里的关键点不是少写了几行代码,而是 if 整体有了一个结果值。只要你把 if 放在赋值右侧,就要保证每一种情况都能产生结果,所以 else 不能省略。否则当 score < 60 时,表达式不知道该返回什么,编译器会直接报错。
这种约束看起来更严格,但对初学者很有帮助。它会逼着你在写判断时把"条件不成立怎么办"一起考虑掉,减少那种只处理成功分支、失败分支被忘在一边的代码。如果条件不止两个分支,可以继续接 else if,把判断按优先级一层层排下去:
kotlin
val level = if (score >= 90) "优秀"
else if (score >= 80) "良好"
else if (score >= 70) "中等"
else if (score >= 60) "及格"
else "不及格"
多分支判断里,顺序很重要。上面的代码会先看是否大于等于 90,再看是否大于等于 80,越靠前的条件优先级越高。如果把 score >= 60 放到最前面,85 分会提前命中"及格",后面的"良好"就永远没有机会执行。写这类分数、等级、区间判断时,先想清楚条件之间是否有包含关系,再决定从大到小还是从小到大排列。
当每个分支的逻辑不止一行时,可以把分支写成代码块。代码块里前面的语句负责执行动作,最后一行表达式负责返回结果:
kotlin
val description = if (score >= 90) {
println("恭喜高分") // 执行动作
"表现优异" // 这一行的值就是 if 的结果
} else {
println("继续努力")
"仍需提升"
}
这段代码体现了 Kotlin 里一个很重要的习惯,代码块({} 包裹的区域)本身也可以有值,值就是块内最后一行表达式的结果。println("恭喜高分") 只是执行过程,不会成为 description 的值,真正赋给 description 的是最后一行 "表现优异"。这个规则后面会在 when、try、lambda 里反复出现,提前熟悉它,读 Kotlin 代码会轻松很多。
也正因为代码块可以返回值,if 的两种用法要分清楚。如果 if 出现在赋值语句、函数返回值这类需要结果的地方,它就是表达式,必须覆盖所有分支。如果 if 单独出现,只是为了"条件满足时做一件事",那它就是普通语句,else 可以省略。前者关心结果值,后者关心副作用,这个区别会直接影响编译器是否要求你写完整分支。
when:多分支的最佳选择
if-else if 链适合两三个条件,继续往下堆就会变得笨重。每个条件都散在一行里,读代码的人要不断确认它们判断的是不是同一个变量,修改时也容易漏掉某个分支。when 就是 Kotlin 为多分支场景准备的工具,它的作用类似 Java 的 switch,但表达能力更强,可以匹配具体值,也可以匹配范围、类型,甚至可以不带主语,直接写一组布尔条件。
最基础的写法是给 when 一个主语,也就是括号里的 day。后面的每个分支都拿自己的条件和这个主语比较,匹配成功就执行右侧内容:
kotlin
val day = 3
val dayName = when (day) {
1 -> "周一"
2 -> "周二"
3 -> "周三"
4 -> "周四"
5 -> "周五"
6 -> "周六"
7 -> "周日"
else -> "无效"
}
println(dayName) // 周三
这个例子里,when (day) 会依次拿 day 和 1、2、3 等分支比较。因为 day 的值是 3,所以第三个分支命中,整个 when 表达式的结果就是 "周三"。和 if 一样,只要 when 被用来给变量赋值,它就必须保证每一种输入都有结果。要么你把所有可能都列出来,要么加一个 else 做兜底。
else 可以理解成最后一道保险。前面的分支都没匹配上时,程序不会停在半路,而是进入 else。在这个例子里,day 如果是 0、8 或其他不在 1 到 7 之间的数字,就会得到 "无效"。写业务代码时,这种兜底分支很重要,因为真实输入经常不像示例里那么干净。
如果多个输入最终要走同一套逻辑,可以把它们写在同一个分支里,用逗号隔开。这样做不是为了炫语法,而是为了让代码表达出真正的分类意图。比如一周里的 1 到 5 都属于工作日,6 和 7 都属于周末,这时我们关心的已经不是"今天具体是周几",而是"它属于哪一类":
kotlin
val type = when (day) {
1, 2, 3, 4, 5 -> "工作日"
6, 7 -> "周末"
else -> "无效"
}
这里的 1, 2, 3, 4, 5 可以理解成"只要 day 等于其中任意一个值,就执行右边的分支"。它不是创建集合,也不是做循环,只是 when 分支条件的一种简写。Kotlin 仍然会从上到下依次比较分支,匹配到第一个符合条件的分支后,就把这个分支右侧的表达式作为整个 when 的结果,后面的分支不会再继续判断。
分支右侧也不一定只能写一行字符串。如果同一类值需要先做一些处理,再返回结果,可以像 if 一样使用代码块。这个时候仍然遵守"最后一行作为结果"的规则,前面的 println 只是执行过程,最后的字符串才是赋给 message 的值:
kotlin
val message = when (day) {
1, 2, 3, 4, 5 -> {
println("准备进入工作日流程")
"工作日"
}
6, 7 -> {
println("可以休息一下")
"周末"
}
else -> "无效"
}
逗号分组适合离散值,比如星期、菜单编号、状态码这类可以一个个明确列出来的值。如果判断的是连续范围,继续用逗号会让代码变长,也容易漏掉边界,这时应该换成下面要讲的区间匹配。简单说,几个具体值归为一类,用逗号,一整段连续数字归为一类,用区间。
when 也可以不给主语。没有主语时,分支左侧就不再是拿来和某个值比较的候选项,而是一组独立的布尔表达式。程序会从上到下检查这些条件,第一个结果为 true 的分支胜出。这种写法特别适合替代复杂的 if-else if 链,因为它把所有条件整齐地排在同一个结构里:
kotlin
val score = 85
val grade = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
score >= 60 -> "D"
else -> "F"
}
注意这里的判断顺序。条件从上到下依次求值,第一个满足的生效,后面不再判断。因此条件写的顺序很关键,如果把 score >= 60 放在第一行,85 分会直接匹配到 "D",而不会再走到后面的 "B" 行。像分数评级这种条件互相包含的场景,通常要把更严格、更具体的条件放在前面。
除了一个个写具体值,when 也可以配合 in 和 !in 判断值是否落在某个范围或集合里。范围判断在处理数字区间时很常见,比如年龄段、分数段、页码范围、库存数量等。和无主语 when 一样,如果多个范围之间可能重叠,就要认真安排顺序,保证先命中的是你真正想要的分支:
kotlin
val x = 15
when (x) {
in 1..10 -> println("个位数范围")
in 11..20 -> println("11 到 20 之间")
!in 1..20 -> println("超出范围")
}
.. 创建的是包含两端点的闭区间,1..10 表示 1 到 10 的所有整数,1 和 10 都算在里面。!in 1..20 则表示"不在 1 到 20 这个范围内"。如果不想包含终点,可以用 until,1 until 10 表示 1 到 9。逆序范围用 downTo,例如 10 downTo 1 代表 10、9、8......一直到 1。这些区间操作符不只服务于 when,在下一节的 for 循环里会更常见。
when 还经常和 is、!is 一起做类型检查。这个能力在真实项目里很实用,因为很多函数接收到的参数一开始可能是一个比较宽泛的类型,比如 Any、接口类型或者父类类型。进入不同分支后,我们希望按具体类型处理它,而不是到处手动强制转换:
kotlin
fun describe(input: Any): String = when (input) {
is Int -> "整数:${input + 1}" // 自动转为 Int,可以直接运算
is String -> "字符串长度:${input.length}" // 自动转为 String,可以调 .length
is Boolean -> "布尔值:${!input}"
else -> "未知类型"
}
这段代码里,input 的声明类型是 Any,按理说它不一定有 length 属性,也不一定能做加法。但进入 is Int 分支后,Kotlin 编译器已经知道这里的 input 是 Int,所以可以直接写 input + 1,进入 is String 分支后,也可以直接访问 input.length。这就是 Kotlin 的智能类型转换(smart cast)。它减少了很多显式强转,让类型判断之后的代码更接近"按真实类型写逻辑"。
for:遍历一切可迭代的东西
讲完判断和分支,就该看重复执行了。for 循环适合处理"我有一组东西,要挨个处理"的场景。在 Kotlin 里,for 通过 in 关键字把循环变量和可迭代对象连起来,格式是 for (变量 in 可迭代对象) { ... }。这里的可迭代对象可以是区间、数组、列表、Map,也可以是任何提供迭代器的对象。
遍历数字区间是最容易理解的用法。循环开始后,i 会依次拿到区间里的每一个值,循环体也会跟着执行同样次数:
kotlin
for (i in 1..5) {
print("$i ") // 输出:1 2 3 4 5
}
.. 创建的区间包含两端,也就是 1 到 5 都会走一遍。如果只需要 1 到 4,不想包含 5,就用 until。这种半开区间在编程里很常见,因为很多集合下标都是从 0 开始,到 size - 1 结束:
kotlin
for (i in 1 until 5) {
print("$i ") // 输出:1 2 3 4
}
until 在遍历数组或列表下标时特别顺手。假设一个列表有 5 个元素,合法下标是 0、1、2、3、4,而不是 1 到 5。写成 0 until list.size 正好覆盖所有合法下标,不需要自己写 list.size - 1,也减少了边界写错的机会。
如果处理顺序要反过来,比如倒计时、从最后一页往前翻、从高分到低分检查,就可以用 downTo 做逆序遍历:
kotlin
for (i in 5 downTo 1) {
print("$i ") // 输出:5 4 3 2 1
}
无论正序还是逆序,默认步长都是 1。也就是说,每次循环变量只前进或后退一个单位。如果想隔一个取一个、每三个取一个,就可以用 step 调整步长:
kotlin
for (i in 1..10 step 2) {
print("$i ") // 输出:1 3 5 7 9
}
// step 也可以和 downTo 配合
for (i in 10 downTo 0 step 3) {
print("$i ") // 输出:10 7 4 1
}
除了数字区间,for 更常见的用途是遍历集合。遍历列表或数组时,最自然的写法是直接拿元素本身,不必先拿下标再取值。只要循环体里不关心当前位置,这种写法通常最清楚:
kotlin
val fruits = listOf("苹果", "香蕉", "橙子")
for (fruit in fruits) {
println(fruit)
}
如果循环体里既需要元素本身,又需要它在列表里的位置,就要把下标也拿出来。第一种写法是用 indices 属性得到所有合法索引,再通过下标访问元素。这种方式比较接近传统写法,适合你确实需要用索引做额外计算的情况:
kotlin
for (i in fruits.indices) {
println("第 ${i + 1} 个:${fruits[i]}")
}
另一种写法是用 withIndex()。它会在遍历时同时给出下标和值,避免在循环体里反复写 fruits[i],读起来更像是在直接描述"我需要当前位置和当前元素":
kotlin
for ((index, fruit) in fruits.withIndex()) {
println("第 ${index + 1} 个:$fruit")
}
(index, fruit) 这个写法叫解构声明,它会把 withIndex() 返回对象里的两个字段同时拆出来,分别赋给 index 和 fruit。第一次见到可能会觉得像语法糖,但它解决的是一个很实际的问题:当一个对象本来就由几个固定部分组成时,可以直接把这些部分命名出来,而不是先拿到整个对象再逐个访问。遍历 Map 时,这种写法尤其常见:
kotlin
val scores = mapOf("Tom" to 90, "Jerry" to 85, "Spike" to 78)
for ((name, score) in scores) {
println("$name -> $score")
}
遍历 Map 时,for 每次拿到的其实是一个 Map.Entry,里面包含 key 和 value。用解构声明后,可以直接把它们拆成 name 和 score,循环体里的代码会更贴近业务含义,不用反复写 entry.key、entry.value。
循环不一定每次都要从头走到尾。遇到目标值后,可以用 break 提前终止当前循环,遇到当前这条数据不需要处理时,可以用 continue 跳过本轮剩下的代码,直接进入下一轮。这两个关键字在 for、while、do-while 中行为一致,后面的猜数字练习会用 break 作为退出无限循环的出口。
while:当你不确定要循环多少次
for 适合循环次数相对明确的场景,比如遍历一个列表、走完一个数字区间、处理 Map 里的每一项。但有些循环在开始前并不知道会执行多少次,只知道"某个条件还没满足,就继续做"。这种场景更适合 while,比如一直让用户输入直到格式合法,一直尝试连接服务器直到成功,或者一直掷骰子直到掷出 6。
while 的执行方式很直接,先检查条件,条件为 true 才执行循环体,执行完一轮后,再回到条件处重新检查。只要条件仍然为 true,循环就会继续,一旦条件变成 false,循环结束,程序继续往下走:
kotlin
var count = 0
while (count < 3) {
println("计数:${++count}")
}
// 输出:
// 计数:1
// 计数:2
// 计数:3
这个例子里,count 从 0 开始,每次进入循环都会先自增再打印。++count 是前置自增,含义是先加一再使用新值,如果写成 count++,第一次输出会是 "计数:0",因为它会先使用当前值,再把变量加一。在循环条件刚好踩着边界时,前置和后置自增的区别可能导致多一轮或少一轮,排错时可以优先检查这里。
do-while 和 while 的核心差别在于检查条件的时机。while 是先判断,再决定要不要执行,do-while 是先执行一次循环体,再判断要不要继续。因此,do-while 的循环体至少会执行一次,无论条件一开始是真还是假:
kotlin
var num = 10
do {
println("当前值:$num")
num--
} while (num > 5)
// 输出:10 9 8 7 6
在上面这个例子里,num 初始值是 10,本来就满足 num > 5,所以换成普通 while 也能得到类似结果。真正能看出差异的是条件一开始就是 false 的情况。普通 while 会直接跳过循环体,而 do-while 会先执行一次再说:
kotlin
var x = 5
while (x > 5) {
println("while 执行了") // 不会输出
}
do {
println("do-while 执行了") // 会输出这一行
} while (x > 5)
这个"至少执行一次"的特性在实际项目里很常见。用户输入校验就是典型场景,程序总要先提示用户输入一次,然后才能判断输入是否合法,如果不合法,再进入下一轮。重试逻辑也类似,程序通常要先尝试一次操作,比如发请求、读文件、连接设备,然后根据结果决定是否继续重试。判断循环类型时,可以先问自己一个问题,循环体有没有必要无条件先执行一次?如果有,do-while 往往更自然,如果没有,普通 while 更清楚。
练习:猜数字游戏
第一个练习把今天学的 if、when、while 串起来,做一个控制台猜数字游戏。程序在 1 到 100 之间随机生成一个数字,让用户反复猜,每次猜完给出"大了"或"小了"的提示,猜中后根据尝试次数给评语。
kotlin
import kotlin.random.Random
fun main() {
val target = Random.nextInt(1, 101) // 1 到 100 的随机整数
var guessCount = 0
var guess: Int
println("已生成 1~100 之间的随机数,来猜吧。")
// 用户输入可能不是数字,所以用无限循环 + break 控制退出
while (true) {
print("请输入你猜的数字:")
val input = readlnOrNull() ?: continue // 空输入直接重试
guess = input.toIntOrNull() ?: run {
println("请输入有效的整数。")
continue
}
guessCount++
// if 表达式直接返回提示文字
val hint = if (guess < target) "太小了,再大一点。"
else if (guess > target) "太大了,再小一点。"
else ""
when {
hint.isNotEmpty() -> println(hint) // 还没猜对,继续循环
else -> break // 猜对了,跳出循环
}
}
// 根据尝试次数分段评语
val comment = when (guessCount) {
1 -> "一发入魂!你是神仙吧?"
in 2..5 -> "厉害,${guessCount} 次就猜中了。"
in 6..10 -> "还行,${guessCount} 次猜中,下次加油。"
else -> "${guessCount} 次才猜中,再练练范围判断吧。"
}
println(comment)
}
这段代码中 Random.nextInt(1, 101) 的第二个参数是上界(exclusive),所以 1..100 的随机数要写成 nextInt(1, 100 + 1)。readlnOrNull() 从控制台读取一行,返回可空类型 String?,用 ?:(Elvis 操作符)给空输入一条兜底动作。toIntOrNull() 同样返回可空类型------转换失败时是 null,和 run {} 的组合起到了"转换失败则提示并重试"的效果。这些类型安全的写法背后是 Kotlin 的空安全体系,后面会有专门的一篇文章展开。
while (true) 创建了一个无限循环,唯一的出口是猜中后执行的那句 break。这个模式的优点是循环退出条件放在循环体中间而非顶部,读代码的人顺着执行流看到 break 就知道"到这里就结束了",比在 while 的条件里塞一个 guess != target 的判断更直观,因为 guess 要在循环体里经过多次赋值才等于 target,条件写死在括号外面反而割裂了退出点和判断逻辑。
游戏最后根据猜测次数用 when 做评语分段,when 的不同分支里都通过字符串模板嵌入了 guessCount 的值,保证用户在最终输出里能同时看到次数和评价。
练习:成绩评级系统
第二个练习做一个学生成绩评级系统,输入一个 0 到 100 的分数,先合法性校验,再用 when 映射到五级等级,最后用 for 列出所有学生的成绩。
kotlin
fun getGrade(score: Int): String = when (score) {
in 90..100 -> "A(优秀)"
in 80..89 -> "B(良好)"
in 70..79 -> "C(中等)"
in 60..69 -> "D(及格)"
in 0..59 -> "F(不及格)"
else -> "无效分数" // 兜底,负数或超过 100
}
fun main() {
val students = mapOf(
"张伟" to 92,
"李娜" to 78,
"王磊" to 45,
"赵敏" to 88,
"刘洋" to 61,
"陈静" to 100
)
// for 遍历 Map,解构出 name 和 score
for ((name, score) in students) {
val grade = getGrade(score)
// 用字符串模板对齐输出------`padEnd(4)` 让名字列宽度一致
println("${name.padEnd(4)}: ${score.toString().padStart(3)} 分 -> $grade")
}
// 用 for 遍历区间统计各等级人数
val gradeRanges = listOf(
"A" to (90..100),
"B" to (80..89),
"C" to (70..79),
"D" to (60..69),
"F" to (0..59)
)
println("\n--- 等级分布 ---")
for ((label, range) in gradeRanges) {
// count { } 遍历 students 的每个 value,统计其中落在当前区间内的个数
val count = students.values.count { it in range }
println("$label 等级:$count 人")
}
}
getGrade 函数体直接用等号连接 when 表达式,省略了花括号和 return。分数上限是 100,所以每个区间的上限也都写到了 100,代码读起来没有"为什么 A 要 90...100 而不是 90...99"这种疑问。
padEnd(4) 给名字右边填空格到 4 个字符宽,padStart(3) 给数字左边填空格到 3 个字符宽。两个方法配合后输出是这样:
张伟 : 92 分 -> A(优秀)
李娜 : 78 分 -> C(中等)
王磊 : 45 分 -> F(不及格)
赵敏 : 88 分 -> B(良好)
刘洋 : 61 分 -> D(及格)
陈静 : 100 分 -> A(优秀)
如果名字是三个字,"张伟"后面补了两个空格,"李娜"也补了两个空格------padEnd 能保证所有中文字符宽度一致。注意这两个方法返回的是新字符串,不会修改原来的值,跟昨天学的字符串不可变特性吻合。
最后的统计段落里,students.values 拿到所有成绩,count { it in range } 遍历每个成绩并计数其中落在当前区间的个数。这里的 count 是一个高阶函数------接收一个 lambda 表达式作为判断条件,返回满足条件的元素个数。这种把函数当作参数传递的写法在 Kotlin 的标准库里大量存在,能显著减少手写循环的次数。
这个练习覆盖了今天的所有知识点,if 表达式的判断逻辑,when 的区间匹配和兜底分支,for 遍历 Map 和解构声明,while 的思路(虽然练习里没有直接出现,但成绩评级系统输入校验的部分完全可以补上 while 循环做重复输入)。把这两个练习的代码自己敲一遍,改改数字和条件玩一玩,Kotlin 的控制流就完全掌握了。