阶段三 · 复合类型与控制流

控制流与模式匹配

一句话总结

Rust 的控制流是表达式导向的:if,match,loop 都能产出值.
其中 match 是核心--它强制穷尽所有分支,能解构数据,是安全使用枚举与 Option 的钥匙.
if let / while let / let...else 则是它针对常见场景的轻量简写.

表达式导向:控制流能返回值

Rust 里 if表达式,能直接产出值赋给变量(无需像 Go 那样先声明再在分支里赋值):

let n = 7;
let parity = if n % 2 == 0 { "偶" } else { "奇" }; // if 直接产出值
let max = if a > b { a } else { b };

分支必须返回同一类型,且注意没有分号

各分支产出的值类型必须一致(ifelse 时,隐含的"无值分支"是 ()).

另外,作为返回值的表达式末尾不加分号--加了分号就变成语句,值变成 ().这是 Rust 新手极易踩的坑,函数返回值同理.

三种循环

Rust 有 loop(无限循环),while,for.其中 loop 能通过 break 返回值,for 统一遍历迭代器:

// loop 通过 break 返回值
let mut count = 0;
let result = loop {
count += 1;
if count == 10 { break count * 2; } // break 携带返回值
};

// for 遍历范围或集合
for i in 0..5 { // 0,1,2,3,4(左闭右开;0..=5 为闭区间)
println!("{i}");
}
for item in &vec { // 借用遍历,不夺走所有权(回顾第 04 篇)
println!("{item}");
}

没有 C 风格 for

Rust 不提供 for (i=0; i<n; i++) 语法.需要索引时用 for i in 0..n,需要元素 + 索引时用 .enumerate()(第 09 篇迭代器会讲).这迫使代码更安全--不会出现越界错误.

match:穷尽的模式匹配

match 拿一个值,逐个比对模式,执行第一个匹配分支.最简单的形态像 switch:

let dice = 3;
match dice {
1 => println!("最小"),
6 => println!("最大"),
_ => println!("中间"), // _ 通配符,匹配其余所有情况
}

但它和 Go switch 有个本质区别:穷尽性检查(exhaustiveness).编译器要求覆盖所有可能,否则报错:

enum Direction { North, South, East, West }

fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "北",
Direction::South => "南",
// 漏掉 East,West:编译错误 non-exhaustive patterns
}
}

Go 的 switch 漏写一个 case 顶多走 default 或啥也不做,bug 静悄悄.
Rust 的 match 像较真的审稿人:没处理的情况当场打回.给枚举新增一个变体时,所有未覆盖的 match 都会编译失败--这是重构时极宝贵的安全网.

match 的真正威力:解构

模式不仅能匹配字面值,还能拆开复合数据,把内部值绑定到变量.这正是处理带数据枚举的方式:

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn handle(msg: Message) {
match msg {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动到 ({x}, {y})"),
Message::Write(text) => println!("文本:{text}"),
Message::ChangeColor(r, g, b) => println!("颜色 {r},{g},{b}"),
}
}

一步到位:判断是哪个变体 + 取出它携带的数据.处理 Option 同理,这正是第 06 篇说的"用 match 拆开 Option".

解构与所有权移走

Message::Write(text) 这行:当 msg 不是引用时,解构会移走所有权--text 取得了 String 的所有权,此后不能再使用 msg.

如果只是想借用而非消耗,可以匹配引用:

fn handle(msg: &Message) {
match msg {
Message::Write(text) => println!("文本:{text}"), // text 是 &String
// ...
}
}

这样 text 自动退化为引用 &String,不会移走数据,match 之后 msg 仍可继续使用.

守卫,范围,绑定,多模式

模式的表达力很丰富,几个高频用法:

let n = 4;
match n {
x if x < 0 => println!("负数"), // 守卫(guard):附加布尔条件
0 => println!("零"),
1..=9 => println!("个位数"), // 范围模式,闭区间
_ => println!("大数"),
}

// 多个模式用 | 连接
match dice {
1 | 3 | 5 => println!("奇"),
_ => println!("偶"),
}

// @ 绑定:既匹配范围,又把值绑定到变量
match age {
n @ 0..=17 => println!("未成年,具体 {n} 岁"),
n => println!("成年,{n} 岁"),
}

if let:只关心一种情况时

当只在乎"匹配某一个模式,其余忽略",写完整 match 就显得啰嗦:

let config: Option<i32> = Some(42);

// 啰嗦的 match
match config {
Some(v) => println!("值是 {v}"),
None => {} // 什么都不做,但必须写
}

// if let 简写:语义等价,更紧凑
if let Some(v) = config {
println!("值是 {v}");
}

if let 也能搭配 else.它本质是 match 的语法糖,编译器自动用 _ => () 兜底--穷尽检查并未丢失,只是省去显式写通配分支的繁琐,适合"成功就处理,否则跳过/兜底".

if let 类似 Go 的 if v, ok := m[key]; ok { ... }--同时完成"取值 + 判断是否存在",一行搞定常见情况.

while let 与 let...else

while let 在模式持续匹配时循环,常用于消费容器:

let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() { // pop 返回 Option,空了得 None 退出
println!("{top}"); // 3 2 1
}

let...else 用于"必须成功取值,否则提前返回",能避免层层嵌套,保持主流程平铺:

fn parse_port(s: &str) -> u16 {
let Ok(p) = s.parse::<u16>() else {
return 8080; // 解析失败,走兜底并 return
};
p // 这之后 p 一定有效,主流程不必缩进
}

最佳实践:按场景选对工具

  • 要处理全部情况,需要穷尽检查 → match
  • 只关心一种成功情况 → if let
  • 反复取值直到 None → while let
  • 取不到就提前返回,让主流程保持平铺 → let...else

let...else 是减少"嵌套地狱"的利器,尤其适合函数开头的一连串前置校验.

快速回顾

  • 表达式导向:if/match/loop 都能返回值;作为返回值的表达式末尾不加分号.
  • 三种循环:loop(可 break 带值)/while/for(遍历迭代器,无 C 风格 for).
  • match 穷尽性:必须覆盖所有分支;新增枚举变体会让旧 match 编译失败,是重构安全网.
  • 解构:match 拆开枚举/元组/结构体并绑定内部值,优雅处理带数据枚举与 Option.
  • 模式增强:守卫 if,范围 1..=9,多模式 |,绑定 @.
  • 简写家族:if let(单一情况),while let(循环取值),let...else(取不到即提前返回).

动手练习

  1. 范围模式:用 match + 范围模式,把分数 0-100 映射到等级 A/B/C/D.
  2. 解构枚举:定义 enum Shape { Circle(f64), Rect(f64, f64) },用 match 解构计算面积.
  3. if let 简写:把"仅当为 Circle 时打印半径"用 if let 实现.
  4. while let 消费:用 while let 配合 Vec::pop 逆序打印一个向量.
  5. let...else 校验:用 let...else 写一个函数--解析字符串为数字,失败则提前返回默认值,成功则继续主流程.