模式匹配是 switch 的超集:能按常量、类型、结构解构、守卫条件分派,并配合密封层级实现编译期穷举检查。它是 Scala 里"让非法状态不可表示"的主力工具。
def describe(x: Matchable): String = x match case 0 => "零" case n if n == 42 => "宇宙终极答案" // 守卫 case s: String => s"字符串:$s" // 类型模式 case (a, b) => s"二元组 ${a}和${b}" // 元组解构 case head :: rest => s"列表头 $head" // 列表解构 case _ => "其他"
对样例类解构是它真正的威力所在——分派的同时完成"拆包":
sealed trait Payment case class Alipay(amount: BigDecimal, userId: String) extends Payment case class BankCard(amount: BigDecimal, last4: String) extends Payment case object Refused extends Payment def settle(p: Payment): String = p match case Alipay(a, u) => s"支付宝 $u 扣 $a" case BankCard(a, "0000") => s"无效卡号拒绝" case BankCard(a, last) => s"卡尾号 $last 扣 $a" case Refused => "风控拒绝"
试着删掉 case Refused 分支:编译器立刻报"匹配不穷举"。这是密封层级 + 模式匹配送你的免费正确性证明——新增一种 Payment 子类型时,全项目漏改的地方会在编译期排队现身,而不是在生产环境排队报警。
| 模式 | 写法 | 拆出什么 |
|---|---|---|
| 常量 | case 42 => |
无 |
| 变量 | case n => |
绑定整个值 |
| 类型 | case s: String => |
无 |
| 解构 | case Point(x, y) => |
字段 |
| 守卫 | case n if n > 0 => |
配合绑定 |
| 嵌套 | case Point(0, y) => |
部分字面量固定 |
| 或 | case Alipay(a,_) | BankCard(a,_) => |
多形态归并 |
| 序列 | case Seq(a, b, rest*) => |
变长尾部 |
解析形如键值对的配置行,类型安全地把脏输入挡在门外:
def parseLine(line: String): Option[(String, String)] = line.trim match case "" => None case s if s.startsWith("#") => None case s"$k=$v" => Some((k.trim, v.trim)) case _ => None
s"$k=$v" 是字符串插值模式,直接在字符串结构上解构。
表达式(上面的 settle,每个分支有值)与部分函数:
val handler: PartialFunction[Event, String] = case Click(x, y) => s"clicked $x $y" case KeyPress(c) => s"key $c" handler.isDefinedAt(Idle) // false,只响应部分输入
偏函数是"只处理一部分输入的函数",actor 收信、异常捕获、路由表的标准写法(6.2、7 章都会重逢)。
⚠️ 匹配顺序自上而下,第一个命中的分支生效——把具体模式放前、宽泛模式放后,
case _永远垫底。

背景:上游系统推送的 JSON 字段可能缺失、类型不符或嵌套,需要对一段解析结果做分层校验并给出可读错误。用模式匹配把每一层的非法情况显式写出来:
sealed trait Json case class JStr(v: String) extends Json case class JNum(v: Double) extends Json case class JObj(fields: Map[String, Json]) extends Json case object JNull extends Json def userEmail(j: Json): Either[String, String] = j match case JObj(fields) => fields.get("email") match case Some(JStr(s)) if s.contains("@") => Right(s) case Some(JStr(s)) => Left(s"邮箱格式非法: $s") case Some(other) => Left("email 不是字符串") case None => Left("缺少 email 字段") case other => Left(s"根节点必须是对象,实际是 $other") userEmail(JObj(Map("email" -> JStr("a@b.c")))) // Right(a@b.c) userEmail(JObj(Map("email" -> JNum(3)))) // Left(email 不是字符串)
操作分三步:sealed 封闭类型层级、match 逐层解构、守卫(if s.contains("@"))在形状之外追加语义校验。结果:四种非法输入得到四种不同错误消息。解读:sealed 让编译器检查分支是否穷尽,少一种情况直接报错,这把"忘了一种 case"这类运行时事故前移到编译期;守卫则承担"类型对但值不对"的细粒度判定。变式:把 Either 换成 Option 会丢错误信息,换成 cats 的 Validated 可以累积多个字段错误——同一套匹配骨架,换上下文即换语义。
def kind(j: Json) = j match case JStr(_) => "str" // 编译警告:JNum、JObj、JNull 未覆盖
对付这条警告有正反两条路:补全分支(正),或加 @unchecked 压制(下策,只在你比编译器更懂不变量时用)。匹配的纪律是宁全勿缺。