第3章 控制结构
if、while、for、for-yield 与 match:Scala 把控制结构做成了“有返回值的表达式”。
本章学习目标
- 能用 if / else 表达式直接赋值,而不是先声明再赋值
- 会写 for 推导式、嵌套 for、守卫与 yield
- 能用 match 表达式替换一长串 if-else
- 理解 while 在 Scala 里不是首选(先想集合与递归)
本章导览
核心内容
3.1
if / else 是表达式
把 if 当表达式用,直接产出值。
用 if/else 表达式给 val 赋值判断没有 else 分支时的类型用 if 链写分段判定
知识点
if 是表达式,不是语句
- if (cond) a else b 本身有值,可以直接赋给 val 或作为参数。
- Scala 没有 Java 的 ? :,三元逻辑直接用 if 写。
- 分支只有一行时可以省略大括号,但缩进要清楚。
类型由两个分支共同决定
- 两边同类型,结果就是该类型。
- 分支类型不同时,编译器取公共父类型;Int 和 String 会得到 Any,后续操作要小心。
- 没有 else 时,if 的类型是 Unit;缺分支的“赋值”通常不是你想要的结果。
常见写法
- val max = if (a > b) a else b,比先声明 var 再改更干净。
- 分段判定写 if / else if / else 链,最后一个 else 保证有返回值。
- 条件太长就抽成有名字的 Boolean:val isAdult = age >= 18。
代码示例
▶ REPL:if 直接赋值
scala> val a = 7
a: Int = 7
scala> val b = 12
b: Int = 12
scala> val max = if (a > b) a else b
max: Int = 12
scala> val label = if (max % 2 == 0) "even" else "odd"
label: String = even👀 运行结果
a: Int = 7 b: Int = 12 max: Int = 12 label: String = even
▶ 分支类型不同与缺少 else 的后果
object IfType {
def main(args: Array[String]): Unit = {
val flag = true
val mixed = if (flag) 1 else "no"
val noElse = if (!flag) 1
println(mixed)
println(mixed.getClass.getSimpleName)
println(noElse)
}
}👀 运行结果
1 Integer ()
▶ if / else if / else 做成绩分级
object Grade {
def level(score: Int): String =
if (score >= 90) "A"
else if (score >= 60) "B"
else "C"
def main(args: Array[String]): Unit = {
List(95, 72, 40).foreach(s => println(s"$s -> ${level(s)}"))
}
}👀 运行结果
95 -> A 72 -> B 40 -> C
常见错误
⚠️ 踩坑与解决
val x = if (score > 60) score; println(x + 1)
→ 缺少 else,if 的类型变成 Unit,x 不再是 Int。;补上 else 分支,例如 else 0,保证两边类型一致。val max = a > b ? a : b
→ Scala 没有三目运算符,? 和 : 不构成这个语法。;写 if (a > b) a else b,或直接用 math.max(a, b)。
💡 只要一段逻辑“算出一个值”,就优先写成表达式赋给 val,少用 var 中转。
3.2
循环:while、for、for-yield 与守卫
用 for、守卫和 for-yield 处理重复计算。
写 while 与 do-while 并控制退出用 for 的区间、守卫和多生成器用 for-yield 生成新集合
知识点
while 适合次数不定
- while (cond) { body } 先判断后执行,body 里必须改变与条件相关的变量。
- do-while 先执行一次再判断,至少执行一次。
- Scala 里 while 通常要用 var,能用集合操作表达的循环优先用 for。
for 遍历与区间
- for (x <- xs) 逐个取元素;1 to 5 含 5,1 until 5 不含 5。
- 倒序用 5 to 1 by -1,步长用 by 2。
- 多个生成器用分号隔开,相当于嵌套循环。
守卫是带 if 的过滤
- for (x <- xs if x % 2 == 0) 只保留满足条件的元素。
- 守卫可以写在生成器后面,多个守卫可以连写。
- 守卫不是 if 语句,不满足就跳过本轮。
yield 决定“生成集合”
- 不带 yield 的 for 只执行副作用,整个表达式的类型是 Unit。
- for (...) yield expr 把每轮结果收集成新集合,不会改动原集合。
- List 进 List 出,Vector 进 Vector 出;需要数组再 toArray。
代码示例
▶ while 与 do-while
object WhileLoop {
def main(args: Array[String]): Unit = {
var n = 5
var fact = 1
while (n > 1) {
fact *= n
n -= 1
}
println(s"5! = $fact")
var i = 0
do {
println(s"i = $i")
i += 1
} while (i < 3)
}
}👀 运行结果
5! = 120 i = 0 i = 1 i = 2
▶ for、守卫、yield 与多生成器
object ForDemo {
def main(args: Array[String]): Unit = {
for (i <- 1 to 5) print(s"$i ")
println()
for (i <- 1 until 5 if i % 2 == 0) println(i)
val doubled = for (i <- List(1, 2, 3)) yield i * 2
println(doubled)
val pairs = (for (i <- 1 to 2; j <- 1 to 3) yield (i, j)).toList
println(pairs)
}
}👀 运行结果
1 2 3 4 5 2 4 List(2, 4, 6) List((1,1), (1,2), (1,3), (2,1), (2,2), (2,3))
▶ for-yield 过滤并变换字符串
object Names {
def main(args: Array[String]): Unit = {
val names = List("alice", "bob", "carol")
val upper = for (n <- names if n.length > 3) yield n.toUpperCase
println(upper)
println(upper.mkString("|"))
}
}👀 运行结果
List(ALICE, CAROL) ALICE|CAROL
常见错误
⚠️ 踩坑与解决
val squares = for (i <- 1 to 3) i * i
→ 没有 yield,循环体结果被丢弃,squares 的类型是 Unit。;写成 for (i <- 1 to 3) yield i * i,再用 val 接住生成的集合。for (i <- 0 to 3) 输出了 4 次
→ to 是闭区间,0 to 3 包含 0、1、2、3。;要 0、1、2 就用 0 until 3,或直接写 0 to 2。
💡 写 for 前先确定三件事:遍历什么、过滤什么、要得到集合还是只要副作用。
3.3
match 表达式入门与模式匹配基础
用 match 按值、类型和结构分支,避免漏 case。
写 match 表达式并返回值用 case class 解构和类型模式知道 MatchError 的触发原因并写兜底分支
知识点
match 是表达式
- 每个 case 后面是一个表达式,命中哪个就返回哪个的值。
- case 从上到下匹配,命中第一个就停,不需要 break。
- 如果所有 case 都不匹配,运行时会抛 MatchError。
模式的几种常用形态
- 字面量:case 1 =>;多值:case 1 | 2 =>;守卫:case n if n > 0 =>。
- 类型:case s: String =>;序列:case List(a, b, _*) =>。
- case class 解构:case Circle(r) => 直接取出字段;case class 本身第 7 章会系统讲。
兜底与穷尽
- 用 case _ 接住剩余情况,通配分支放在最后。
- sealed trait 加所有子类的 case,能让编译器检查漏项。
- 变量模式 case n 会接住所有值,放在前面会让后面的分支不可达。
Scala 3 的差异
- 2.13 用 sealed trait + case class/case object 建模;Scala 3 还可以用 enum。
- 本课统一 2.13 写法,遇到 3 的语法会单独标注。
代码示例
▶ 按字面量与守卫匹配
object MatchDemo {
def main(args: Array[String]): Unit = {
def describe(x: Int): String = x match {
case 0 => "zero"
case 1 | 2 => "small"
case n if n < 0 => "negative"
case _ => "big"
}
List(0, 1, -5, 99).foreach(n => println(s"$n: ${describe(n)}"))
}
}👀 运行结果
0: zero 1: small -5: negative 99: big
▶ sealed trait + case class 解构
object ShapeArea {
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Rect(w, h) => w * h
}
def main(args: Array[String]): Unit = {
println(f"${area(Circle(1.0))}%.2f")
println(f"${area(Rect(2.0, 3.0))}%.2f")
}
}👀 运行结果
3.14 6.00
▶ 类型模式与序列模式
object TypeMatch {
def show(x: Any): String = x match {
case i: Int => s"Int: $i"
case s: String => s"String: $s"
case List(a, b, _*) => s"List head=$a second=$b"
case _ => "unknown"
}
def main(args: Array[String]): Unit = {
println(show(42))
println(show("hi"))
println(show(List(1, 2, 3)))
println(show(3.14))
}
}👀 运行结果
Int: 42 String: hi List head=1 second=2 unknown
常见错误
⚠️ 踩坑与解决
scala.MatchError: 2 (of class java.lang.Integer)
→ match 只写了 case 1,没有覆盖输入 2,运行时找不到分支。;补上 case _ => "other" 或其他具体分支,保证所有输入都有归宿。match 里先写 case n => println(n),后面的具体 case 永远不执行
→ n 是变量模式,能匹配任何值,后面的分支不可达。;把通配或变量模式放到最后,具体字面量、类型分支放前面。
💡 先列出所有可能的输入再写 case;编译器提示 match may not be exhaustive 时,补分支而不是压警告。