“模式匹配”这四个字,我第一次看到的时候,以为是某种高大上的设计模式或者架构方案。后来在写 Rust 和 Kotlin 的过程中,才意识到这是一套完全不同的思维工具。它不只是语法糖,更是一种对数据结构的“拆解能力”——让你能用一种近乎直白的方式,把复杂的数据形状描述清楚,然后让编译器帮你穷尽所有可能。
这篇文章我想好好聊聊“模式与模式匹配”这件事。我会以 Rust 的match和模式语法为主线,也会对比 Java、C#、Kotlin、Swift 这些语言里的同类能力,顺带把很多人在搜索时经常混淆的概念掰开揉碎——比如设计模式中的“策略模式”、嵌入式里的“GPIO 工作模式”、算法竞赛里的“ACM 模式”,它们和编程语言中的“模式匹配”压根不是一个维度的东西,但背后有一点非常底层的共通哲学:把“可能性”显式地管理起来。
如果你是刚接触函数式风格语言,或者在写业务代码时经常被一堆if-else逼疯,这篇文章会给你一种新的代码组织思路。如果你已经写过不少match,后面关于可反驳性、解构绑定、性能注意事项的篇幅,也应该能帮你挖出一些盲区。
1. 模式匹配到底是什么
1.1 一个被滥用的词
搜索框里输入“模式”两个字,你能得到一堆完全不相干的东西:Java 设计模式、GPIO 的 8 种工作模式、Edge 开发者模式、CSM 兼容模式、AC 模式、策略模式……它们都是“pattern”或者“mode”,但指的东西天差地远。
设计模式里的“策略模式”,是面向对象架构下的一种代码组织套路;GPIO 的“工作模式”,是芯片引脚在电气层面的行为配置;蓝牙的 A2DP/SCO,是音频传输链路的切换状态。这些“模式”的共同点是:它们在一组预定义选项中做选择。而编程语言里的模式匹配,字面意思完全不同——它指的是一种“按形状解构数据”的语法能力。
用一个生活例子来区分:你的快递柜取件码是 4-3-2,这叫“模式”(pattern),因为你按这个形状去匹配柜子。而“把这个包裹放到大件柜还是小件柜”,这叫“模式选择”(mode selection)。模式匹配的重点不是“选一个选项”,而是“把数据拆开看内部结构”。
1.2 为什么需要模式匹配
写业务逻辑最朴素的做法是if-else。问题在于,当数据的形状复杂起来,条件判断会迅速膨胀。比如你要判断一个网络请求的结果:
result = get_from_api() if result is not None: data = result["data"] if data is not None: code = data["code"] if code == 200: payload = data["payload"] handle(payload) else: handle_error(code) else: handle_error(-1) else: handle_error(-2)这种“回调地狱”式的嵌套在业务代码里太常见了。模式匹配解决的是这个问题:它让你把“先看这个值长什么样,再决定怎么处理”这个逻辑,写成一个扁平、直观、并且编译器能帮你验证完整性的结构。
在 Rust 里,同样的逻辑直接写成:
match result { Some(Response { code: 200, payload }) => handle(payload), Some(Response { code, .. }) => handle_error(code), None => handle_error(-2), }编译器会强制你覆盖所有情况。漏了任何一个分支,代码根本编译不过去。这个“穷尽性检查”(exhaustiveness checking)是模式匹配最值钱的地方——它把“我忘了处理某个情况”这种运行时才可能爆的问题,提前到了编译期。
1.3 模式匹配能做什么
总结下来,模式匹配的核心能力有四块:
- 解构:按形状拆开元组、结构体、枚举、数组、切片。
- 匹配:判断一个值是否满足某种具体形态(比如“正好等于 3”“是负数”“在某个范围内”)。
- 绑定:从匹配到的结构里提取变量,直接绑定到局部名字。
- 穷尽检查:编译器确保你处理了所有可能的分支。
这四点组合起来,你会发现很多原本需要手写 parse、手写 instanceof 判断、手写空值检查的代码,都可以收敛成一个直观的match表达式。
2. Rust 里的模式语法拆解
2.1 从最简单的字面量匹配开始
模式匹配最基础的形式,就是拿值去和字面量比:
let x = 3; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("other"), }这里的_是通配符,等价于“其他所有情况”。没有_,编译器大概率会报“非穷尽”错误,因为x是i32,它可能的取值远不止 1、2、3。
字面量匹配本身很简单,但它是理解模式匹配的起点——模式就是一种“值形状的描述”。你描述得越具体,匹配的分支就越精确。
2.2 变量绑定与通配符
再看这个例子:
match x { 1 => println!("one"), other => println!("other: {}", other), }这里的other不是比较,而是“绑定”。意思是不管x是什么值,只要它不是 1,就把这个值绑定到other这个变量上。绑定的含义是“我接受任何值,但我要把它记下来用”。
这就是模式匹配里最容易混乱的一点:在模式位置出现的裸标识符,不是在比较,而是在赋值绑定。想比较一个变量和另一个变量的值,你得用守卫(guard),这个后面细说。
通配符_和绑定变量看起来像,但有个重要区别:_不绑定值,也就是说你拿不到_里的内容。它纯粹是“占个位置”。
2.3 解构元组、结构体与枚举
模式匹配真正发力的是解构。
元组解构:
let pair = (1, "hello"); match pair { (0, _) => println!("起点"), (x, "hello") => println!("x 是 {}, 第二项是 hello", x), (x, y) => println!("x 是 {}, y 是 {}", x, y), }结构体解构:
struct Point { x: i32, y: i32, } let p = Point { x: 10, y: 20 }; match p { Point { x: 0, y: 0 } => println!("原点"), Point { x, y } => println!("x={}, y={}", x, y), }枚举解构是 Rust 里最经典的用法。Rust 没有空值null,替代方案是Option<T>。要拿Option里的值,模式匹配是主路:
fn print_if_some(value: Option<i32>) { match value { Some(v) => println!("有值: {}", v), None => println!("没有值"), } }Some(v)这里,Some是枚举变体,v是绑定变量。整个模式的意思是:如果value是Some这种形态,就把里面包着的值取出来赋给v。
2.4 范围匹配与多重模式
有些场景下,你关心的是一个值的范围,而不是具体某个值。Rust 用..=表示范围模式:
match score { 90..=100 => println!("A"), 80..=89 => println!("B"), 70..=79 => println!("C"), 0..=69 => println!("D"), _ => println!("非法分数"), }注意..=是包含右端点的范围,而..在模式里代表的是“忽略中间部分”,两者别搞混。
多重模式用|连接,表示“命中任何一个都行”:
match c { 'a' | 'e' | 'i' | 'o' | 'u' => println!("元音"), _ => println!("辅音"), }|可以和绑定组合——如果命中了多个模式之一,绑定的变量在分支体里可以直接用:
match x { 1 | 2 => println!("1 或 2"), n @ 3..=5 => println!("3 到 5 之间: {}", n), }这里有个新符号@。它的意思是“匹配右边这个模式,并把整个值绑定到左边的变量”。比如n @ 3..=5,就是“如果值在 3 到 5 之间,把这个值本身赋给 n”。如果只写3..=5,你在分支体里是拿不到那个值是多少的,只能知道它落在区间内。想拿到值,就得用@绑定。
2.5 守卫(guard)补充条件
模式本身能描述形状,但有些条件是模式描述不了的,比如“x 是偶数”“x 大于 y”。这时候用守卫(if子句)补充:
match pair { (x, y) if x == y => println!("相等"), (x, y) if x + y == 0 => println!("互为相反数"), (x, y) => println!("其他情况: {} {}", x, y), }守卫不是模式的一部分。它的位置在模式后面、箭头前面,可以读取模式里绑定的变量。一个常见的坑是:守卫只是附加条件,不影响模式的穷尽性——也就是说,你仍然需要提供一个不带守卫的分支来兜底,否则编译器会认为有未覆盖的情况。
3. 可反驳模式与不可反驳模式
3.1 两类模式的本质区别
Rust 里的模式分为两种:
- 不可反驳模式(irrefutable):一定能匹配成功。比如
let x = 5;里的x,无论右边是什么都能匹配,所以叫不可反驳。 - 可反驳模式(refutable):有可能匹配失败。比如
Some(v),如果值是None就匹配不上。
这俩分类的意义在于:某些地方要求模式必须不可反驳,某些地方允许可反驳,还有一些地方可以根据需要混用。
let、for、函数参数这些“强制绑定”的场景,模式必须是不可反驳的:
let (a, b) = (1, 2); // 合法,元组解构一定能成功 let Some(v) = maybe_value; // 非法!如果 maybe_value 是 None 就炸了而match、if let、while let这些“按条件分支”的场景,模式可以是可反驳的:
if let Some(v) = maybe_value { println!("有值: {}", v); }if let是match的语法糖:只关心一种情况,其他情况一概忽略。它在处理“只对某种形状感兴趣”的代码时非常简洁。
3.2 常见错误:在 let 里使用可反驳模式
新手最容易犯的错是把可反驳模式直接放到let上。写着写着想省事,把match换成if let没问题,但要是换成let,编译器立刻报错。错误信息大概是“refutable pattern in local binding”,中文意思是“本地绑定中使用了可反驳模式”。
这不是运行时才告诉你,而是编译期直接拒绝。Rust 这种“编译器强迫你把所有可能性想清楚”的风格,一开始会觉得烦,写多了会变得非常安心——因为大量低级错误在编译阶段就被拦住了。
3.3 while let 的妙用
while let是和if let类似的循环版本。它适合处理“反复取出某种形状的值,直到取不到为止”的逻辑。一个典型的场景是用迭代器处理栈:
let mut stack = Vec::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(top) = stack.pop() { println!("弹出: {}", top); }这段代码的意思是:只要stack.pop()返回Some(top),就进入循环体并绑定top;当返回None时,循环自动结束。比起手写loop + match,这写法干净得多。
4. 多语言里的模式匹配横评
4.1 Java 的 switch 模式匹配
Java 在 17 开始预览、21 正式定稿的模式匹配switch,是 Java 语言近些年最有价值的更新之一。在旧写法里,你处理一个类型联合要这样:
Object obj = getValue(); if (obj instanceof String s) { System.out.println(s.length()); } else if (obj instanceof Integer i) { System.out.println(i + 1); } else { System.out.println("unknown"); }新语法直接把类型判断和变量绑定合二为一:
switch (obj) { case String s -> System.out.println(s.length()); case Integer i -> System.out.println(i + 1); case null -> System.out.println("null"); default -> System.out.println("unknown"); }Java 的模式匹配有个细节值得注意:它专门加了case null分支。在旧的switch里传null会直接 NPE,而新的模式匹配switch允许你显式处理空值。这是设计上的进步——把“空值”从“意外”变成了“一种需要处理的情况”。
Java 21 还支持记录模式(record pattern),可以对 record 类型做嵌套解构:
record Point(int x, int y) {} Object obj = new Point(3, 4); if (obj instanceof Point(int x, int y)) { System.out.println(x + ", " + y); }4.2 C# 的 switch 表达式与属性模式
C# 的模式匹配走得更远。它不光支持类型模式、位置模式,还支持属性模式,可以直接匹配对象的属性值:
string GetCategory(Weather w) => w switch { { Temperature: < 0 } => "冰点以下", { Temperature: >= 0 and < 20 } => "凉爽", { Temperature: >= 20 and <= 30 } => "舒适", _ => "炎热" };这里< 0、>= 0 and < 20是关系模式和逻辑模式。C# 把大小比较、逻辑组合都塞进了模式体系里,表达力很强。和 Rust 的守卫相比,C# 更倾向“把条件内联进模式”,而不是另起一行写守卫。
C# 还有一个列表模式(list pattern),在 .NET 里处理集合时很方便:
int[] numbers = { 1, 2, 3 }; string desc = numbers switch { [1, 2, 3] => "正好是 1,2,3", [1, ..] => "以 1 开头", [] => "空数组", _ => "其他" };..在 C# 列表模式里的作用是“匹配任意长度的中间部分”,和 Rust 切片模式里的..思路一致。
4.3 Kotlin 的 when 与密封类
Kotlin 把模式匹配叫when,但它本质上就是模式匹配。配合密封类(sealed class),Kotlin 能做到和 Rust 枚举类似的穷尽性保证:
sealed class Result { data class Success(val data: String) : Result() data class Error(val code: Int) : Result() object Loading : Result() } fun handle(result: Result) = when (result) { is Result.Success -> println(result.data) is Result.Error -> println("错误: ${result.code}") Result.Loading -> println("加载中") }如果when用作表达式并且没有覆盖所有密封类子类,Kotlin 编译器也会报错。和 Java 相比,Kotlin 的when更灵活:支持任意表达式做分支条件,支持类型判断,支持解构。Java 21 的模式匹配switch更像是向 Kotlin 等语言看齐的产物。
4.4 Swift、Scala 与函数式传统
Swift 的switch也是正经的模式匹配。它的特殊之处在于case分支默认不落空,需要显式fallthrough才能继续执行。Swift 还支持区间匹配、元组匹配、值绑定,以及where子句做守卫,和 Rust 的体验非常接近。
Scala 则是 JVM 上最早把模式匹配发扬光大的语言。Scala 2 的match配合 case class 解构,影响了后来几乎所有语言的设计。
横向看下来,主流编程语言在模式匹配上的方向基本一致:类型判断加解构加穷尽性检查。区别在于力度和语法风格。Rust 最严格,C# 最花哨,Kotlin 最亲民,Java 追赶得很扎实。
5. 模式匹配的应用场景与设计思维
5.1 状态机:模式匹配的主场
状态机是模式匹配最典型的应用场景。一个网络连接的状态,无非是Disconnected、Connecting、Connected、Retry这些枚举值。用match写状态机,每个状态一个分支,每一条转移清晰可见:
enum ConnectionState { Disconnected, Connecting, Connected, Retrying(u32), } fn transition(state: ConnectionState, event: Event) -> ConnectionState { match state { ConnectionState::Disconnected => match event { Event::Connect => ConnectionState::Connecting, _ => ConnectionState::Disconnected, }, ConnectionState::Connecting => match event { Event::Connected => ConnectionState::Connected, Event::Fail => ConnectionState::Retrying(1), _ => ConnectionState::Connecting, }, ConnectionState::Connected => match event { Event::Disconnect => ConnectionState::Disconnected, _ => ConnectionState::Connected, }, ConnectionState::Retrying(n) => match event { Event::Timeout => ConnectionState::Retrying(n + 1), Event::Connected => ConnectionState::Connected, _ => ConnectionState::Retrying(n), }, } }这段代码的优点是:每个状态和事件的组合都能在编译期验证。如果某天加了个Event::Ping,编译器会提醒你所有match都要处理它。这种“编译器帮你遍历所有可能性”的体验,是if-else给不了的。
实际做嵌入式或者网络开发的同学,可能会想到 GPIO 引脚的工作模式配置——输入、输出、开漏、复用、模拟,那也是状态切换。虽然硬件上的“模式”和这里说的“模式匹配”是两回事,但它们的共同哲学是:把可枚举的选择显式化、可验证化,而不是靠一堆散落的条件判断。
5.2 编译器与解释器:树形结构的解构
写解释器或者编译器,核心工作就是处理抽象语法树(AST)。AST 本身就是嵌套的枚举结构,模式匹配几乎是为此量身定做的。
enum Expr { Num(i32), Add(Box<Expr>, Box<Expr>), Sub(Box<Expr>, Box<Expr>), Mul(Box<Expr>, Box<Expr>), } fn eval(expr: &Expr) -> i32 { match expr { Expr::Num(n) => *n, Expr::Add(a, b) => eval(a) + eval(b), Expr::Sub(a, b) => eval(a) - eval(b), Expr::Mul(a, b) => eval(a) * eval(b), } }这个求值函数的核心逻辑就是这么简单。每个Expr变体对应一个求值规则,match负责解构和分发。没有模式匹配的话,你得写一堆if (expr instanceof Add)、然后手动拆字段,代码量翻倍,还容易漏。
5.3 协议解析与事件驱动
协议解析是另一个模式匹配的主场。网络包、消息帧、JSON 结构,本质上都是带形状的数据。用模式匹配直接按协议形状取字段,比手动遍历字节流高效得多。
事件驱动系统里,事件本身就是一个枚举。用模式匹配分发事件,每个分支处理一种事件类型,代码的可读性和可扩展性都非常好。比如一个简单的 UI 事件处理:
fn handle_event(event: UiEvent) { match event { UiEvent::Click { x, y } => handle_click(x, y), UiEvent::KeyPress(key) => handle_key(key), UiEvent::Resize { width, height } => handle_resize(width, height), UiEvent::Focus { element_id } => handle_focus(element_id), } }如果事件类型加了新的,编译器会告诉你哪里没处理。这种“少写一个分支就编不过”的体验,保证了后续维护者不会因为遗忘而漏业务。
5.4 算法竞赛与 ACM 模式中的“模式”
热词里出现了“ACM 模式”“模式匹配 PTA”,这些其实是另一类“模式”——输入输出格式的套路。ACM 比赛里所谓“模式”,通常指的是某种数据输入的组织方式,比如多组测试数据、行内空格分隔、先给组数再给数据等等。
算法竞赛里的“模式匹配”,其实也有一部分是真的在做字符串模式匹配——KMP、AC 自动机、正则表达式匹配这一类。字符串模式匹配的核心是“子串和模式的对应关系”,和本文讨论的数据结构模式匹配不完全一样,但思想上同源:用一个预定义的规则去识别输入的形态。
在写竞赛题时,如果把输入解析写成match或when,代码会清晰不少。例如按命令分发:
when (cmd) { "push" -> stack.push(last()) "pop" -> stack.pop() "top" -> println(stack.top()) else -> println("unknown cmd") }工程代码和竞赛代码在这个层面的追求是一致的:逻辑扁平、分支清晰、容易排查。
5.5 模式匹配与设计模式的关系
很多人一搜“模式”,会得到 Java 设计模式的 23 种套路,比如策略模式、观察者模式、工厂模式。这些是架构层面的模式,关注的是类与对象之间如何组织协作。
模式匹配是语言层面的特性,关注的是数据如何被拆解和分支。
这两者之间有联系吗?有。最明显的是:策略模式的核心是“把一组可互换的行为封装成对象,然后在运行时选择其中一个”。而模式匹配提供的正是“运行时按值的形态选择行为”的能力。很多原本需要策略模式解决的场景,在支持模式匹配的语言里可以直接用枚举加match解决,代码更短、更紧凑。
不过这不意味着模式匹配“取代”了策略模式。策略模式的优点在于行为对象可以独立扩展、组合、复用,甚至可以在运行时动态替换;模式匹配则把行为写死在分支里,适合那些行为固定、穷尽性重要的场景。两者各有优劣,选型时取决于你对灵活性、可扩展性和编译期安全的要求。
6. 实操中的注意事项与避坑记录
6.1 元组解构时字段顺序别写反
元组解构是按位置匹配的,顺序写错会导致逻辑完全跑偏。
let (x, y) = (1, 2); match (x, y) { (y, x) => println!("y={}, x={}", y, x), }这种代码看起来没问题,实际上(y, x)是一个全新的绑定模式:把第一个位置的值绑定到y,第二个位置的值绑定到x。结果打印出来反而是“y=1, x=2”。这种“变量名和位置不匹配”的坑,在实际代码里很容易出现。我建议元组结构超过三个字段时,尽量改用结构体,可读性和安全性都更好。
6.2 下划线变量不要用于业务逻辑
Rust 里_x和_不一样。_是完全忽略,_x是“绑定但不使用”。区别在作用域结束时的行为:
let _ = some_computation(); // 值立刻被丢弃 let _x = some_computation(); // 值绑定到 _x,作用域结束时才释放如果some_computation()返回一个Drop类型(比如MutexGuard、File),用_会立刻 drop,用_x会延迟到作用域结束。这在锁、文件句柄这类资源管理上可能导致完全不同的行为。我踩过一次坑:用_接收一个锁的 guard,以为锁被持有,实际在语句结束就释放了,导致并发行为完全不符合预期。
6.3 守卫中再次绑定的常见误区
守卫里可以读取模式绑定的变量,但不能在守卫里做同名的绑定。有人想写“匹配成功后再拆分一次”,于是这样写:
match value { Some(s) if s.len() > 5 => ... }这没问题。但有人写出:
match value { Some(s) if let Some(inner) = s => ... // 这是错误的 }守卫里只能放布尔表达式,不能放模式绑定表达式。如果需要做嵌套匹配,应该直接写嵌套的模式,比如Some(Some(inner)),而不是在守卫里动手脚。
6.4 与?运算符的搭配
在 Rust 里处理Option和Result时,很多场景用?比match更简洁:
fn get_name(user: Option<User>) -> Option<String> { let u = user?; Some(u.name) }但如果同时需要处理多个互斥的情况、每种情况要有不同的逻辑,match依然是首选。?解决的是“快速解包并提前返回”,match解决的是“分情况处理”。二者是互补关系,不是替代关系。
我个人的习惯是:单个Option或Result的“解包失败就退出”用?;多个分支、多种形状、需要穷尽性保证的用match。if let则用于“只关心一种分支”的场景。
6.5 性能:模式匹配是否昂贵
不少刚接触模式匹配的人会担心性能。其实完全不用担心。Rust 的match在编译后会生成高效的跳转逻辑,复杂的嵌套模式解构在 release 模式下几乎不产生额外开销。编译器会把模式编译成分支判断表或者二进制搜索树,而不是逐条线性比较。
Java 21 的模式匹配switch也会做类似的优化,JIT 编译后性能通常优于等价的instanceof链。C# 的 switch 表达式同样能编译成高效的跳转表。模式匹配的性能定位是“和你手写的条件判断差不多甚至更好”,而不是“高级语法但太慢不能用”。
唯一需要注意的是@绑定涉及值的拷贝时,如果绑定的类型很大(比如一个很大的结构体),可能产生不必要的拷贝开销。但 Rust 里默认是移动语义,模式匹配只会移动引用,不会深拷贝,除非你显式调了.clone()。
6.6 调试技巧:打印匹配到的值
写模式匹配时,如果发现某个分支没有被触发,最简单的排查方式是在分支里加打印。但更好的方式是利用dbg!宏:
match value { x => { dbg!(x); // 其他处理 } }或者用#[derive(Debug)]给类型加调试输出,然后:
Some(v) => println!("进入了 Some 分支: {:?}", v),实际调试过程中,#[derive(Debug)]加println!("{:?}")的组合至今是最顺手的排查手段。Rust 的编译错误信息质量很高,模式匹配相关的错误(比如“非穷尽匹配”)会直接列出来缺少哪些模式,照着补就行。
7. 其他场景中的“模式”扫盲
热词里有不少和“模式”相关但和模式匹配无关的东西,我简单过一遍,免得大家搜索时混淆概念。
GPIO 的 8 种工作模式:这是嵌入式单片机里的概念,指引脚在电气层面的输入/输出形态,比如浮空输入、上拉输入、推挽输出、开漏输出等。选择哪种模式,取决于外部电路的设计要求。
CSM 兼容模式:BIOS 里的概念,为了兼容旧操作系统而提供的传统启动模式,和 UEFI 模式相对。新固件默认 UEFI,个别老系统需要打开 CSM 才能启动。
Namenode 安全模式:Hadoop HDFS 里的状态。Namenode 启动时会进入一个“只读”的保护状态,在这个状态下不能修改文件系统,主要是让元数据加载完成。
调试模式的监控窗口:嵌入式 IDE(比如 Keil、IAR)里用于在线观察变量的工具。可以看到结构体、数组、指针的值,前提是设备连接正常、运行状态处于调试模式。
Edge 开发者模式、文档模式:浏览器的一个设置选项,用于开启开发者工具、兼容旧网站。它不是编程语言层面的东西,而是浏览器提供给前端工程师调试页面的入口。
策略模式、23 种设计模式:面向对象架构里的代码组织套路。策略模式的核心是“定义一族算法,封装起来,让它们可以互相替换”。学习设计模式建议先从单例、工厂、观察者、策略这几个开始,它们在日常业务代码中使用频率最高。
AC 模式、键盘 AC 模式:一般指某些外设或软件里的一种快捷操作状态,需要看具体上下文。这类词在不同领域含义差别很大,搜索时最好带上领域限定词。
GPIO、ACM 模式、调试模式、桥接模式、中继模式:这些在嵌入式、计算机网络里各有具体含义。比如网络里的桥接和中继,处理的是数据链路的转发方式;GPIO 的模式处理的是芯片引脚的电气行为。它们和编程语言“模式匹配”完全不是一个维度。
说到底,“模式”这个词的语境感非常重要。你需要在脑子里建立一个分类:架构模式(设计模式)、电气/硬件模式(GPIO 工作模式)、软件运行模式(安全模式、调试模式)、语言特性(模式匹配)。四者之间偶尔有灵感上的相通,但技术细节完全独立。
8. 我的一些心得
模式匹配是我近几年写代码时,印象最深的一次“思维升级”。以前写 Java 代码,处理一堆类型的时候习惯用instanceof加类型转换,写多了总觉得别扭——类型判断、强制转换、可能出现的ClassCastException、漏掉的else,零零碎碎的东西混在一起,代码又长又容易出错。第一次正经用 Java 21 的switch模式匹配写了段业务代码之后,那种“一个表达式把类型判断和值解构全干完”的清爽感,确实很难回头。
如果你刚开始接触模式匹配,我的建议是找一个存量项目里的复杂if-else链,试着用模式匹配重写。重点不是“替换语法”,而是体会那种“把可能性想尽再动手”的思维方式。等你自己把一段嵌套地狱改成一层match,看到编译器的穷尽性检查帮你抓住了几个遗漏分支时,你就明白这东西的价值在哪里了。
后面如果有时间,我想再写一篇关于 Rust 枚举设计的内容——把“什么时候该用枚举”“什么时候该用结构体”“状态和数据的建模怎么用模式匹配落地”展开聊聊。这些东西放到一起看,才是模式匹配真正的威力所在。