第3章 控制结构

if、while、for、for-yield 与 match:Scala 把控制结构做成了“有返回值的表达式”。

流程第3-4周3 讲9 段示例代码

本章学习目标

  • 能用 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
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 的后果
SCALA
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 做成绩分级
SCALA
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
SCALA
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 与多生成器
SCALA
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 过滤并变换字符串
SCALA
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 的语法会单独标注。

代码示例

▶ 按字面量与守卫匹配
SCALA
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 解构
SCALA
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
▶ 类型模式与序列模式
SCALA
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 时,补分支而不是压警告。