老周在邮局分拣台干了二十年。他的工位前有三个木头格子:1号放平信,2号放挂历,3号放“其他”。每天来的包裹不管多复杂,他都只能靠封面上的编号把它们扔进对应的格子——这就是传统 switch 的生活版,它天生只会问一句“编号等于几”。 直到有一天,邮局换了一台“智能分拣器”。这台机器不仅看封面编号,还能看包裹形状、量尺寸、称重量,甚至轻轻撕开一个角检查里面的东西。它每一个“分拣规则”都不再是固定的编号对应关系,而是一套“长什么样、是什么类型、内部结构如何”的综合判断——这就是 Scala 模式匹配(Pattern Matching)的神奇之处。你会发现,它不只是比 switch 强大十倍,而是换了一种思维方式:把“我要分什么”写清楚,剩下的交给编译器去验证、穷举和优化。

岗位关联

对 AI 从业者的价值:做数据处理、写 Spark 作业时,大量逻辑是“按不同类型走不同分支”。掌握模式匹配,你写出的代码就像一张清晰的分拣规则表,比一串串 if-else 更短、更安全、更好读,也让算法实验的代码更容易维护和演进。

目录
  1. 可视化演示
  2. 匹配基础
  3. 结构匹配
  4. 提取器
  5. 守卫与绑定
  6. 密封类
  7. 偏函数与风格
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示Scala模式匹配的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Scala模式匹配:一模式匹 一、模式匹配基础与匹配类型 二、case class解构与 核心机制可视化 Scala模式匹配整体架构与关键组件

匹配基础

老周的旧分拣台就像经典的 switch:它只知道问“编号等于几”。1 号平信、2 号挂历、3 号“其他”——每个格子对应一个固定数值,一封没写编号的信就会被丢进杂物堆。这种判断方式最大的问题不是慢,而是“只看值、不看形状”。一个东西是信封还是包裹,是圆筒还是方盒,switch 完全看不见。

Scala 的模式匹配把分拣台换成了智能分拣器。它的核心语法是 match 关键字:我们把一个表达式放进分拣口,下面写一排 case 规则,每个规则是一个“模式”。分拣器从上到下依次尝试这些模式,第一个“长得像”的规则胜出,并执行右侧的表达式。需要注意的是,match 在 Scala 中是一个表达式——它总会返回一个值,不需要你写 return。

更棒的是,case _ 是万能兜底,就像分拣台上的“其他”格子,但它的位置很讲究,通常放在最后。若把通配符放在前面,后面的规则就永远没有机会被尝试,编译器会给出“不可达代码”的警告。

回到邮局故事:老周的旧机器只会按编号落格,智能分拣器却能从一堆信封中“认出”长方形、圆筒和异形件。这就是 match 对 switch 的第一层碾压——它看到的不是一个数字,而是整个形状。

match(x)={r1x=c1r2x=c2relseotherwise\displaystyle \text{match}(x) = \begin{cases} r_1 & x = c_1 \\ r_2 & x = c_2 \\ r_{\text{else}} & \text{otherwise} \end{cases}
上面公式表示:对同一个输入 x,从上到下依次检查它是否符合某种模式,一旦命中就返回对应的结果。这与 switch 的“一个编号对应一个格子”有本质不同——模式是一个结构描述,而不只是一个值。
  1. 匹配头部match 把表达式 x 作为待分拣对象传入,后续所有 case 都针对它判断。
    x→matchcasesx \xrightarrow{\text{match}} \text{cases}
  2. 分支列表每个 case 由一个模式和一段结果表达式组成,就像分拣台上的一格规则。
    case pattern⇒result\text{case } \text{pattern} \Rightarrow \text{result}
  3. 求值完成第一个命中的分支结果就是整个表达式的值,不需要 return 也能自然返回。
    match(x)=ri\text{match}(x) = r_i

结构匹配

有的包裹光看封面分不了类,必须撕开一个角检查里面的东西。老周过去不敢拆,怕拆坏。但 Scala 的 case class 天生就是“可以无损拆开的快递箱”:它的构造参数写在类名后的括号里,匹配时也能用一样的写法把参数“解构”出来,就像沿箱子的折痕打开包装纸一样,一个指纹都不会留。

最常见的结构匹配对象是元组和 case class。元组像用胶带捆在一起的小布袋,case (a, b) 能同时取出布袋里的两个物品;case class 则像带标签的三层快递箱,case Box(w, h) 能把宽和高两个字段分别绑到变量 w 和 h 上。这种操作叫“解构”,是构造(把多个值装进一个对象)的逆过程。

更厉害的是嵌套解构:一个快递箱里还塞着另一个小盒子,你可以一次写到最深层。例如 case Order(price, User(name, age)),直接把订单里的价格和用户信息一并取出。老周要是能用这招,连夹层里塞的发票都能一次掏出来。

无损开箱,这就是 case class 解构给老周带来的新技能。旧分拣台只能看看封面,智能分拣器却能伸手进去摸清内部结构,还不破坏包装。数据只要是 case class,就能在匹配时被“拆开看”。

case C(f1,f2,…,fn)⇒result(f1,…,fn)\displaystyle \text{case } C(f_1, f_2, \ldots, f_n) \Rightarrow \text{result}(f_1, \ldots, f_n)
这条公式的意境:当一个 case class 对象具备“外皮” C 和“内容” f_1...f_n 时,模式匹配可以同时检查外皮并取出内容。外部形状和内部零件,一次全部拿到。
  1. 构造模式C 是 case class 的名字,括号里写的是待绑定字段的占位变量。
    C(f1,…,fn)C(f_1, \ldots, f_n)
  2. 字段绑定匹配成功后,对象的第 1 到第 n 个字段会被依次放入 f_1 到 f_n 这些变量。
    fieldi↦fi\text{field}_i \mapsto f_i
  3. 结果表达式分支体可以直接使用这些变量,就像开箱后把零件摊在桌面上使用。
    result(f1,…,fn)\text{result}(f_1, \ldots, f_n)

提取器

如果包裹不是标准纸箱,而是月饼铁盒、牛皮纸袋、甚至一卷画轴,case class 的自动开箱就失灵了。这时老周需要一套“万能开箱工具”——只要某个工具定义了 unapply 方法,它就能在匹配时把任意对象拆成需要的零件。这种做法在 Scala 里叫“提取器”(Extractor)。

unapply 的意思是“反向构造”:构造器把几个值装进一个对象里,unapply 则把这个对象拆回几个值。它的返回值通常是一个 Option:如果拆得开,返回 Some(拆出的值们);如果完全不匹配,返回 None。当我们在 case 里写 case SomePattern(变量) 时,编译器会去调用那个模式的 unapply 方法来完成拆箱判断。

提取器最大的优点是“什么都能拆”。正则表达式、集合、自定义业务对象,只要写一个 unapply,就能享受模式匹配的完整威力。比如你有一个代表“加密信封”的类,老周只要能定义它的 unapply,就能在匹配时自动解密并取出信纸,而信封本身不会被破坏。

有了提取器,老周面对的再也不是“标准件”还是“非标准件”的两难问题。只要肯写一把螺丝刀(unapply),任何形状的包裹都能被这把螺丝刀精确拆开。铁盒、画卷、奇怪的信封,全都逃不过智能分拣机的“法眼”。

unapply(x):Option[T]=Some(v1,…,vn)\displaystyle \text{unapply}(x): \text{Option}[T] = \text{Some}(v_1, \ldots, v_n)
这条公式说明提取器的工作方式:接收待匹配对象 x,返回一个 Option。Some(v_1, ..., v_n) 表示匹配成功,并且这些值会提供给 case 模式中的变量;None 表示匹配失败,继续尝试下一个 case。
  1. 待匹配值unapply 接收一个值 x,它就是要被分拣的原始对象。
    unapply(x)\text{unapply}(x)
  2. 判断包装返回值类型是 Option[T],Some 表示能拆,None 表示拆不了。
    Option[T]=Some(…) or None\text{Option}[T] = \text{Some}(\ldots) \text{ or } \text{None}
  3. 解包绑定Some 里的值会按顺序绑定到 case 模式中的变量上。
    Some(v1,…,vn)⇒vi bound\text{Some}(v_1, \ldots, v_n) \Rightarrow v_i \text{ bound}

守卫与绑定

光凭“形状”还不足以做决定:一个大箱子可能只是泡沫材质很轻,一个小箱子却可能塞满了铁块。老周需要在匹配规则后面再加一道“门槛”——如果箱子尺寸超过某条线,就去大件通道;否则走普通通道。Scala 的守卫条件(Guard)正好做这件事:在 case 模式 后面写 if 表达式,只有表达式为真,分拣器才放行。

同时,有时我们需要给整个包裹贴一个跟踪编号,而不是只取里面的零件。Scala 用 p @ 模式 语法完成“变量绑定”:p 绑定的不是内部字段,而是整个被匹配的对象。这样在分支体里,既能访问整体 p,也能通过解构出来的字段访问零件。二者结合起来,可以写出非常细腻的规则。

比如处理一个数学表达式节点时,可以写 case p @ Mul(a, b) if eval(a) > 0 =>:先要求这是乘法结构,再要求左边子表达式的值大于零,最后把整个节点绑定到 p。守卫和绑定这对组合拳,让“模式”不再只是静态形状,而能感知运行时的动态条件。

老周终于不用看到“箱子大”就往大件格扔了。他会先看形状,再捏一捏重量、量一量尺寸,最后还不忘给整件包裹贴上跟踪编号。分拣机从“眼力”进化到“脑力”,这就是守卫与绑定给模式匹配带来的第二次飞跃。

case p@pattern if guard(x)⇒body\displaystyle \text{case } p @ \text{pattern} \text{ if } \text{guard}(x) \Rightarrow \text{body}
这条公式是守卫与绑定同时出现的典型形态:pattern 负责结构匹配,p @ 把整体对象绑定到变量 p,if guard(x) 是额外条件,三者全部满足才执行 body。
  1. 结构模式pattern 首先核对对象的结构,比如是不是 Order 类型、是否包含两个字段。
    pattern(x) matches structure\text{pattern}(x) \text{ matches structure}
  2. 整体绑定p @ 把匹配成功的整个对象 x 绑定到变量 p,方便分支体里整体使用。
    p=xp = x
  3. 守卫条件if guard(x) 在结构匹配之上再加一道逻辑判断,只有判断为真才进入分支。
    guard(x)=true\text{guard}(x) = \text{true}
  4. 分支执行以上条件全部满足,执行 body;否则跳到下一个 case 继续尝试。
    body(p,fields)\text{body}(p, \text{fields})

密封类

某天邮局老板发来一张“可收寄清单”,上面只写了三种包裹:平信、挂历、易碎品。从此智能分拣机多了一个能力——它能在上岗前自动检查:你的分拣规则是否覆盖了清单上的所有类型?如果漏了一种,直接拒绝开工。这就是 Scala 中“密封类”(sealed)配合编译器穷尽检查的神奇效果。

用 sealed trait 声明一个类型后,它的所有直接子类都必须定义在同一个源文件里。编译器因此能完整列出“世界上一共有哪些子类”,并对 match 做穷尽检查。如果你匹配的是一个 sealed 类型,却只写了其中两个子类的 case,编译器会直接报错,提醒你还有第三种没处理。这种错误发生在编译期,而不是运行到某个诡异数据时才发现。

密封类带来的最大收益是“演进的安全感”。当你在代码库里新增一个子类时,所有相关的 match 表达式都会立刻亮起红灯,逼你把新类型的情况补上。你可以选择补一个专门分支,也可以显式写 case _ 表示“我允许它走兜底路线”——这个选择权一直都在你手里。

老周的“清单思维”救了他一命:以前每逢新增包裹类型,他都要靠经验检查所有分拣口;现在有了密封类,机器的上岗考试会直接告诉他漏了哪个格子。从“运行时的惊吓”到“编译期的提醒”,这是函数式设计送给程序员最稳的保险。

∀s∈subtypes(sealed),∃ cases∈cases\displaystyle \forall s \in \mathrm{subtypes}(\mathrm{sealed}),\quad \exists\, \mathrm{case}_s \in \mathrm{cases}
这条公式描述穷尽检查的完成条件:对于密封类型的所有子类型 s,都要存在一个对应的 case 分支。这个“全部覆盖”的结论由编译器静态得出,不需要跑到运行时才知道结果。
  1. 枚举子类型subtypes(sealed) 是编译器能看到的全部子类集合,因为有 sealed 限制,集合是有限的。
    subtypes(sealed)={s1,s2,…,sn}\mathrm{subtypes}(\mathrm{sealed}) = \{s_1, s_2, \ldots, s_n\}
  2. 逐一覆盖每个子类型 s 在 match 中都要有对应分支 case_s。
    ∃ cases∈cases\exists\, \mathrm{case}_s \in \mathrm{cases}
  3. 编译期结论当以上条件成立,编译器认为匹配已穷尽,无需额外兜底分支。
    exhaustive(cases)=true\text{exhaustive}(\text{cases}) = \text{true}

偏函数与风格

老周其实还拿到了一个“任性模式”:有些分拣员只处理自己负责的那类包裹,不归他管的直接不接。正常的函数要求每个输入都必须有输出,就像对每个包裹都要强行塞进一个格子;而偏函数(PartialFunction)明确表示:我只对一部分输入负责,其他输入不在我的职责范围内。

在 Scala 里,一个由 case 组成的块天然就是一个偏函数。比如 { case "error" => "告警" } 只处理内容恰好是字符串 "error" 的输入。集合的 collect 方法会先调用偏函数的 isDefinedAt 判断是否匹配,再对匹配的元素应用函数——相当于“过滤 + 映射”一步完成。老周再也不用对所有包裹都强行给个去处了。

这种风格在数据处理里特别顺手,尤其是写 Spark 作业时。你经常需要从日志、事件流里挑出特定类型的记录并做转换,collect 一行就能完成“是不是我要的类型 + 怎么处理”两件事。配合前面的 case class 和密封类,整套代码读起来就像一张分拣规则表,每一条规则都直白得不需要额外注释。

老周的故事到这里,分拣机已经升级完毕:看得懂形状(结构匹配)、会开箱(提取器)、有脑子(守卫)、有清单(密封类)、还可以挑活干(偏函数)。他把所有规则浓缩成一行行 case,从此分拣不再是苦差事,而是一场优雅的代码艺术。

f:A↪B,dom(f)⊆A\displaystyle f: A \hookrightarrow B,\quad \mathrm{dom}(f) \subseteq A
这条公式说明偏函数的特点:f 从 A 映射到 B,但它的定义域 dom(f) 只是 A 的一个子集。输入落在定义域之外时,偏函数直接表示“不归我管”。
  1. 定义域子集dom(f) 是 A 的子集,表示偏函数只在部分输入上有定义。
    dom(f)⊆A\mathrm{dom}(f) \subseteq A
  2. 映射关系对定义域内的每个元素,偏函数都能给出一个结果 f(a) = b。
    ∀a∈dom(f),  f(a)∈B\forall a \in \mathrm{dom}(f),\; f(a) \in B
  3. collect 组合collect 等价于先过滤出定义域内的元素,再映射成结果。
    collect(f)=map(f)∘filter(dom(f))\text{collect}(f) = \text{map}(f) \circ \text{filter}(\text{dom}(f))

AI 应用场景

Spark 日志清洗的数据管道

阶段:数据预处理与特征工程

解决的问题:日志数据来源多种多样,格式、字段、异常形态各不相同。传统 if-else 写满一整屏,新加一种日志就得改动多处逻辑,极易漏改;模式匹配把不同日志按类型和结构拆分,每类一条 case,编译器还能帮忙检查是否覆盖完整。

具体案例:一个 Spark 作业处理服务器日志,定义 sealed trait LogEntry 和若干 case class(访问日志、错误日志、性能指标)。使用 collect { case ErrorLog(time, msg) => ... } 只抽取错误日志并转换,再聚合统计错误率,代码量减少一半且结构一目了然。

事件驱动系统中的命令路由

阶段:在线服务与事件处理

解决的问题:微服务收到多种命令事件,每种事件的字段和业务逻辑差异巨大。硬编码分支容易遗漏新事件;用密封类型 + 模式匹配描述领域事件,编译期穷尽检查保证每个事件都有处理者。

具体案例:在 Akka 或 Kafka 事件流中定义 OrderPlaced、PaymentReceived、OrderCancelled 等 case class,通过 match 清洗、路由、落库。新增一种订单事件时,编译器立刻标出所有需要补分支的位置。

表达式计算器的 AST 解析引擎

阶段:算法实现与推理计算

解决的问题:在实现规则引擎、小型 DSL 或 AI 推理解释器时,表达式树有多种节点类型,需要逐个解构并递归计算。模式匹配的嵌套解构让递归算法变得像数学定义一样简洁。

具体案例:实现一个四则运算计算器,定义 sealed trait Expr 和 Add、Mul、Num 等 case class;用 case Add(l, r) => eval(l) + eval(r) 递归求值。整个 eval 函数不到十行,逻辑却完整覆盖所有表达式。

Python 演示

python28 行
from dataclasses import dataclass @dataclassclass Box:    size: float    brand: str = "" @dataclassclass Envelope:    pages: int def route(item):    match item:        case Box(size=w) if w > 100:            return "big-channel"        case Box(size=w):            return f"standard-channel({w})"        case Envelope(pages=p) if p > 5:            return "thick-letter-channel"        case Envelope(pages=p):            return "flat-letter-channel"        case _:            return "holding-area" if __name__ == "__main__":    items = [Box(40), Box(150), Envelope(3), Envelope(12), "mystery"]    for it in items:        print(f"{it} -> {route(it)}")
目的:用 Python 3.10 的 match 语句模拟 Scala 模式匹配的邮局分拣逻辑,通过数据类解构和守卫条件把不同包裹路由到不同通道。
  1. @dataclass把类变成数据类,让后续的 match 能按字段拆解对象。
  2. class Box:定义一个“方箱”类型,包含尺寸字段。
  3. def route(item):分拣函数,所有路由规则集中在这里。
  4. match item:对输入对象启动模式匹配。
  5. case Box(size=w) if w > 100:解构出 size 字段,并用守卫判断是否走大件通道。
  6. case Envelope(pages=p):解构出信封页数,普通平信通道。
  7. case _:兜底分支,匹配一切未覆盖的输入。
  8. for it in items:遍历待分拣列表,逐个调用 route 打印去向。

常见误区

误区我理解的模式匹配就是 switch 的语法糖,无非是把 `case 1` 换成 `case "A"`,本质上还是拿一个值和几个常量做等于判断。

正解它的核心是“结构对比 + 解构”。它不仅匹配常量值,还能匹配类型、元组、case class 内部结构、集合形状,甚至可以在匹配时把内部字段提取出来。老周的旧分拣台只见编号,智能分拣机看的是整个包裹的形状和内部内容,这是两代机器。

为何常见:很多语言里 switch 是唯一的“分支语法”,初次接触 Scala 时自然会把新模式套进旧框架里理解。

误区我以为解构是 `case class` 的专属特权,普通类没有这个能力,所以匹配时只能对 case class 用括号。

正解任何对象只要定义了 unapply 方法,就能成为提取器,从而参与模式匹配。case class 只是编译器自动帮你生成了一个 unapply,并不是它独占了解构能力。正则、集合切片、自定义业务类,都能通过提取器被拆解。

为何常见:日常代码里 90% 的解构都发生在 case class 上,初学者很少注意到背后自动生成的 unapply,误以为这是魔法特权。

误区我只要把 trait 标记成 `sealed`,所有 `match` 就都能通过穷尽检查,运行时就永远不会抛 MatchError。

正解穷尽检查成立的前提是“分支覆盖了该密封类型的所有直接子类”。如果某个分支加了守卫 if,守卫可能会把本应覆盖的情况过滤掉,编译器无法确认是否真全覆盖。此外,如果你在 match 中混入了其他类型,或使用了嵌套匹配,仍然可能漏状态。sealed 是强力的编译期助手,但不是无敌护盾。

为何常见:在 sealed trait 的简单示例上工作几天后,容易把“这个示例能穷尽”推广成“所有 match 都能穷尽”,忽略了守卫条件的例外。

误区我把通配符 `case _` 放在最前面,把具体类型分支放在后面,反正都能匹配到,结果应该一样。

正解模式匹配严格从上到下尝试,第一个匹配成功的分支胜出。case _ 放在最前面会吞掉后面所有分支,导致它们变成不可达代码。具体模式、守卫模式必须写在通配符前面,顺序本身就是逻辑的一部分。

为何常见:当所有分支互斥(常量都不同)时,顺序确实无所谓;一旦出现重叠模式(通配符、类型匹配、守卫),顺序错误很难排查,因为代码能编译,但结果不符合预期。

面试题

Q1. 在 Scala 模式匹配中,匹配任意值的“兜底”通配符是哪个?

单选题 · easy

  • A.case _ => 匹配任意值← 正确答案
  • B.case * => 匹配任意值
  • C.case ? => 匹配任意值
  • D.case ! => 匹配任意值

答案:A。case _ 是 Scala 的通配符模式,能匹配任意值,通常放在最后作兜底。它就像一个“其他”格子,避免 match 抛异常。其他符号如 *、?、! 都不是 Scala 的合法通配符。

Q2. 要让编译器对模式匹配做穷尽检查,应该用哪个关键字修饰类型?

单选题 · easy

  • A.final 修饰的类定义
  • B.sealed 修饰的类定义← 正确答案
  • C.private 修饰的类定义
  • D.abstract 修饰的类定义

答案:B。sealed(密封)让所有直接子类只能出现在同一源文件中,编译器因此能枚举全部子类并检查 match 是否覆盖完整。final 阻止继承,private 控制访问,abstract 允许继承,都与穷尽枚举无关。

Q3. 关于 Scala 中 match 表达式,说法正确的是?

单选题 · easy

  • A.它只能作为语句使用,不能返回值
  • B.它一定返回 Unit,类似 switch 语句
  • C.它是一个表达式,每个分支返回公共类型的结果← 正确答案
  • D.它要求每个分支都返回相同类型的值

答案:C。match 在 Scala 中是表达式,会返回值。所有分支的结果类型会统一成一个“公共类型”,所以不一定要求字面完全相同的类型,只要求存在一个共同父类型。这就像邮局的多个格子,虽然放的物件不同,但都能称为“邮件”。

Q4. 在模式匹配中添加守卫条件的正确语法是?

单选题 · mid

  • A.case x if x > 0 => 带守卫← 正确答案
  • B.case x when x > 0 => 带守卫
  • C.case x && x > 0 => 带守卫
  • D.case if x > 0 => 带守卫

答案:A。Scala 的守卫关键字是 if,写在模式之后、箭头之前,如 case x if x > 0 =>。when、&& 等选项在其他语言中常见,但在 Scala 里是错误的语法。守卫相当于给分拣规则再加一道门禁。

Q5. 模式中出现的 p @ Point(0, 0) 表示什么?

单选题 · mid

  • A.将 Point 对象与 p 进行比较
  • B.将整个匹配结果绑定到变量 p← 正确答案
  • C.用它替换掉 Point 模式
  • D.只匹配名字为 p 的对象

答案:B。p @ 是变量绑定语法,p 会绑定到整个匹配对象,而括号里的 Point(0, 0) 继续做结构解构。这样在分支体里既可以整体使用 p,也可以用解构出来的字段。就像给整个包裹贴了一个跟踪编号,同时还能取出包裹内部的零件。

Q6. 以下哪个不属于 Scala 支持的模式?

单选题 · mid

  • A.常量与通配符模式
  • B.类型与结构模式
  • C.文件内容哈希模式← 正确答案
  • D.解构与守卫模式

答案:C。Scala 模式支持常量匹配、类型匹配、结构解构、守卫条件等,但绝不会去匹配文件内容的哈希值。模式匹配关心的是对象的结构和字段,而不是对文件做哈希计算。这条是考察对模式匹配能力边界的理解。

Q7. 关于提取器(Extractor),正确的说法有哪些?

多选题 · mid

  • A.只有 case class 自动带有 unapply
  • B.普通对象也能定义 unapply 成为提取器← 正确答案
  • C.提取器可以把匹配值拆解成若干字段← 正确答案
  • D.编译器会强制每个提取器返回 Option 类型

答案:BC。提取器并不限于 case class,任何对象只要定义 unapply 方法都能成为提取器,所以 A 错、B 对。unapply 能把匹配值拆成若干字段供模式使用,C 对。unapply 返回值可以是 Boolean、Option 或其他形式,编译器不强制必须是 Option,所以 D 错。

Q8. sealed(密封类)与模式匹配结合的好处有?

多选题 · hard

  • A.编译器可在编译期发现未穷尽的 match← 正确答案
  • B.让类型层级在同一文件内保持封闭← 正确答案
  • C.新加子类时旧 match 代码会被编译器提醒← 正确答案
  • D.能保证运行时一定不会抛 MatchError

答案:ABC。sealed 让子类集合固定,编译器能穷举并检查 match 是否覆盖完整,A 对。所有直接子类必须同文件,B 对。新增子类后,已有的 match 会因漏分支而编译报错,相当于自动提醒,C 对。D 过于绝对,因为守卫条件、嵌套匹配等情况仍可能导致运行时异常。

Q9. 有以下 Scala 代码:

单选题 · extreme

  • A.编译失败,缺少兜底分支
  • B.编译成功,分支已穷尽所有子类← 正确答案
  • C.编译失败,sealed 类不能继承
  • D.编译成功但运行会抛 MatchError

答案:B。sealed trait Shape 只有 Circle 和 Rect 两个直接子类,两个分支已经覆盖全部可能,编译器会判定匹配穷尽,因而不需要额外 case _。sealed 允许继承,只是限制在同一个文件内。因此 B 正确,A、C、D 错误。

Q10. 关于 match 的模式顺序和守卫,正确的是?

多选题 · hard

  • A.特定模式应放在通配符前面← 正确答案
  • B.带守卫的 case 放在通配符之后可能永远不生效← 正确答案
  • C.通配符分支应放在最后作为兜底← 正确答案
  • D.模式顺序与执行结果无关,因为编译器会排序

答案:ABC。模式匹配从上到下依序尝试,第一个命中者胜出。特定模式放前面才不会被子类型或通配符拦截,A 对。若带守卫的 case 放在通配符之后,通配符会抢先命中导致守卫永远没机会执行,B 对。通配符作为兜底通常放最后,C 对。编译器不会重排分支顺序,D 错。