3.3 模式匹配深入


3.3 模式匹配深入

模式匹配是 switch 的超集:能按常量、类型、结构解构、守卫条件分派,并配合密封层级实现编译期穷举检查。它是 Scala 里"让非法状态不可表示"的主力工具。

从 switch 到结构分派

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 _ 永远垫底。

图 3-2 match 表达式的分派树

图 3-2 match 表达式的分派树

完整案例:JSON 解析结果的分层处理

背景:上游系统推送的 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 可以累积多个字段错误——同一套匹配骨架,换上下文即换语义。

常见编译错误:match not exhaustive

def kind(j: Json) = j match case JStr(_) => "str" // 编译警告:JNum、JObj、JNull 未覆盖

对付这条警告有正反两条路:补全分支(正),或加 @unchecked 压制(下策,只在你比编译器更懂不变量时用)。匹配的纪律是宁全勿缺。

本节要点回顾

  • 解构 + 分派一体:样例类匹配在分支里直接拆字段。
  • 密封层级换穷举检查:编译器替你证明"没漏分支"。
  • 素材丰富:守卫、嵌套、或模式、变长序列、插值模式各有场景。
  • 偏函数:只处理部分输入,是路由与事件分发的原生表达。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U