枚举与模式匹配
在第 6 章中,你学习了 type——Auto 定义带命名字段的结构化数据的方式。Rectangle 总是有 width 和 height。User 总是有 name 和 age。但如果一个值可能是几种不同形状之一呢?如果一个形状要么是圆,要么是矩形,但不能同时是两者呢?这就是_枚举(enumerations)_——简称 enums——的用武之地。
枚举让你通过列举可能的变体来定义一个类型。每个变体可以携带自己的数据,每个变体可以用模式匹配独立处理。在本章中,你将学习如何定义枚举、为变体附加数据、用 is 解构它们、用 on 块处理它们,以及用守卫条件细化模式。
在本章中,我们将讨论:
- 什么是枚举? —— 将类型定义为一组命名的变体
- 带数据的枚举 —— 在每个变体中携带值
- 用
is进行模式匹配 —— 将枚举解构为各个部分 on块 —— 消息处理器风格的匹配- 解构类型 —— 使用字段名解构结构体
- 穷尽匹配 —— 编译器确保你处理了每种情况
- 守卫 —— 用
if添加条件模式
什么是枚举?
type(结构体)说的是"这个值有字段 X、Y 和 Z",而枚举说的是"这个值是 A、B 或 C 中的_某一个_"。当一个值可以是不同的事物,并且你需要分别处理每种可能性时,就使用枚举。
方向是一个经典的例子。方向是北、南、东或西——没有其他可能。我们可以列举每种可能性,这就是"枚举"名称的由来。
输出:
NorthDirection 现在是一个自定义类型。你用 Direction.North 创建值——枚举名、一个点号、然后是变体名。这与 Rust 不同,Rust 使用 Direction::North 双冒号语法。
每个目标语言对枚举的映射不同:
- Rust 使用相同的
enum关键字。变体用::访问。Auto 自动生成Display实现,因此你可以直接打印变体名。 - Python 使用标准库中的
Enum类。每个变体成为带有整数值的类属性。 - C 使用带整型判别式的
typedef enum。辅助函数将枚举值转换为可打印的字符串。 - TypeScript 使用
enum关键字。Direction[d]在运行时将数值转换回名称。
像 Direction 这样的简单枚举——每个变体只是一个名称,没有附加数据——对于表示一组固定的状态很有用。但当每个变体可以携带自己的数据时,枚举才真正强大。
带数据的枚举
每个枚举变体都可以携带数据。Circle 变体可能携带半径。Rect 变体可能携带宽度和高度。关键洞察是每个变体可以携带_不同类型_和_不同数量_的数据。
输出:
Circle(5.0)
Rect(3.0, 4.0)带数据变体的语法是 VariantName(Type)——变体名后跟括号内的类型。要构造值,你写 Shape.Circle(5.0)——枚举名、一个点号、变体名、以及括号中的数据。
注意每种语言如何处理带数据的变体:
- Rust 原生使用相同的
Variant(Type)语法。Auto 的语法直接映射。 - Python 使用标记联合方法——每个变体成为一个
@dataclass,枚举类型是所有变体类的Union。 - C 使用标记联合模式——一个包含标签枚举和
union的struct,union中包含每个变体的数据。这是 Auto 和 Rust 自动完成的手动等价物。 - TypeScript 使用判别联合——每个变体是一个
class,枚举类型是联合类型Circle | Rect。
C 和 Python 的实现展示了枚举在"底层"的样子——一个标识变体的标签,加上该变体的数据。Auto 和 Rust 自动处理这些,所以你不需要关心。
用 is 进行模式匹配
在枚举变体中放数据只是一半的故事。另一半是_取出数据_。这就是模式匹配的作用。Auto 使用 is 关键字将值与一系列模式进行比较,为每个匹配的模式执行不同的代码。
输出:
78.53975
12让我们拆解一下 is 表达式:
is s { ... }检查值s,按顺序尝试每个模式。Circle(r)是一个_模式_,匹配Circle变体并将其内部值绑定到变量r。当s是Shape.Circle(5.0)时,此模式匹配,r变为5.0。Rect(w, h)匹配Rect变体,将宽度绑定到w,高度绑定到h。- 每个分支的形式是
pattern => expression。右侧的表达式成为整个is块的返回值。
is 关键字是 Auto 中等价于 Rust match 的构造。以下是模式匹配在各语言中的映射:
| 语言 | 构造 | 示例 |
|---|---|---|
| Auto | is value { Pattern => expr } | Circle(r) => r * r |
| Rust | match value { Pattern => expr, } | Shape::Circle(r) => r * r, |
| Python | if isinstance(v, T) | isinstance(s, Circle) |
| C | switch (tag) | case ShapeTag_Circle: |
| TypeScript | v instanceof T | s instanceof Circle |
在 Rust 中,Auto 的 is 直接映射为 match——分支相同,只是 Rust 要求分支之间用逗号分隔。在 Python 和 TypeScript 中,模式匹配变成 isinstance / instanceof 检查。在 C 中,它变成对标签字段的 switch。
on 块
Auto 提供了第二种模式匹配构造,叫做 on。on 块与 is 具有相同的语义,但使用消息处理器风格。当每个分支调用函数或执行打印等副作用时,它尤其易读。
输出:
Goodbye!
Moving to 10, 20
Hello!on 块在语法上与 is 完全相同——两者使用相同的模式语法,都能绑定变量,都是穷尽的。区别在于风格。当你计算值时使用 is(每个分支中最后一个表达式成为返回值)。当你为每种情况执行操作时使用 on(比如打印、日志或分发到处理器)。
可以这样理解:is 问"这是什么?"并返回一个答案。on 说"处理这个"并执行一个操作。
在所有目标语言中,is 和 on 都编译为相同的构造:Rust 中的 match,Python 中的 if/elif 链,C 中的 switch,TypeScript 中带 instanceof 的 if/else if 链。
解构类型
模式匹配不仅限于枚举。你也可以通过匹配字段名来解构类结构体的类型。当你想从类型中提取值、转换它们并创建新实例时,这很有用。
输出:
2
1swap 函数接受一个 Pair 并创建一个字段交换的新 Pair。函数体是一个表达式——Pair { x: p.y, y: p.x }——它成为返回值,因为它是函数体中的最后一个表达式。
这种模式有时被称为"解构构造"——你从一个实例中读取字段,并用它们构造一个新实例。它在所有目标语言中的工作方式相同。Rust 的语法与 Auto 完全相同。Python 使用关键字参数。C 逐个赋值字段。TypeScript 将值传递给构造函数。
注意 p 是按值传递的,所以原始对不变。这是 Auto(以及 Rust 和 C)的默认行为。swap 函数创建一个新的 Pair 而不是修改原始的。
穷尽匹配
is 和 on 都是_穷尽的_——编译器确保你处理了每一个可能的变体。如果你给枚举添加了新变体但忘记更新匹配,编译器将拒绝编译你的程序。这是枚举相对于整数常量的最大安全优势之一。
输出:
red
green穷尽性是 Auto 和 Rust 的编译时保证。如果你给 Color 添加 Yellow 变体但忘记在 color_name 中处理它,编译器将产生错误。Python 和 TypeScript 不在编译时强制这一点——if/elif 链会直接跳过而不匹配。C 的 switch 需要 default 分支以避免警告,这就是为什么 C 输出包含 default: return "unknown";。
这种保证在大型代码库中特别有价值。当你给枚举添加新变体时,编译器告诉你每个需要更新的地方。你永远不会遗漏一个情况。
守卫
有时仅靠模式不够具体。你可能想匹配一个变体_并且_对其数据检查条件。Auto 支持_守卫(guards)_——附加在模式分支上的 if 条件,提供额外的过滤层。
输出:
positive integer
negative integer
zero
float: 3.14
string: hello守卫在模式之后添加条件,用 if 分隔:
Int(n) if n > 0匹配Int变体_并且_要求n为正数。如果值是Int(42),此分支匹配。如果值是Int(-1),守卫失败,Auto 尝试下一个分支。Int(n) if n < 0匹配Int变体且值为负数的情况。Int(_)使用通配符模式_来匹配任何Int值。_的意思是"我不关心这个值。"此分支捕获Int(0)——唯一剩余的情况。
分支按从上到下的顺序尝试。第一个模式匹配_且_守卫为真的分支胜出。这很重要:如果你把 Int(_) 放在带守卫的分支之前,它会匹配所有情况,守卫永远不会被检查。
在目标语言中,守卫变成每个 case 分支内的嵌套 if 语句。在 Rust 中,守卫语法 if condition 是 match 原生的。在 C 中,守卫变成 case 块内的 if。在 Python 和 TypeScript 中,守卫变成 isinstance 检查之后的嵌套条件。
总结
枚举和模式匹配让你能够对可以是几种不同形状之一的数据进行建模,并安全、显式地处理每种形状。以下是你在本章学到的内容:
- 简单枚举——
enum Direction { North, South, East, West }——将类型定义为一组固定的命名变体。用点号访问变体:Direction.North。 - 带数据的枚举——
Circle(float)和Rect(float, float)——每个变体可以携带自己的数据。用Shape.Circle(5.0)构造。 - 用
is进行模式匹配——is s { Circle(r) => ... }——解构枚举值并将内部数据绑定到变量。每个分支中最后一个表达式成为返回值。 on块——与is语义相同,但在风格上更适合执行操作(打印、分发)而非计算值。- 解构类型——从结构体中读取字段并用它们构造新值:
Pair { x: p.y, y: p.x }。 - 穷尽匹配——编译器确保你处理了每个变体。如果你添加新变体,每个未处理它的
is和on都将成为编译错误。 - 守卫——
Int(n) if n > 0为模式分支添加条件。分支按顺序尝试,第一个匹配且守卫为真的分支胜出。
以下是所有目标语言的映射对照表:
| 概念 | Auto | Rust | Python | C | TypeScript |
|---|---|---|---|---|---|
| 定义枚举 | enum { ... } | enum { ... } | class(Enum) | typedef enum | enum { ... } |
| 访问变体 | E.V | E::V | E.V | E_V | E.V |
| 数据变体 | V(T) | V(T) | @dataclass | 标记联合 | class |
| 模式匹配 | is v { } | match v { } | isinstance | switch | instanceof |
| 守卫 | P if cond | P if cond | 嵌套 if | 嵌套 if | 嵌套 if |
| 穷尽性 | 编译时 | 编译时 | 无 | default | 无 |
枚举是第 6 章缺失的那块拼图。结合 type(结构体),你现在有了两个互补的工具来建模你的领域:type 表示"这个值_是_一个有这些字段的事物",enum 表示"这个值_是以下事物中的某一个_"。用 is 和 on 进行的模式匹配将一切串联起来,让你安全、简洁地检查和处理每种可能性。