外观
第 18 章 口算机
小螃蟹说
第 2 章猜数字,我们第一次见到 match;第 5 章动物叫声,match 大显身手。但你可能没意识到:我们用的只是模式的冰山一角。
模式(pattern)是 Rust 最灵活的"形状描述器":它不只是 Some(x) 这种简单样子,还能描述"一个三个元素的列表""两个数字都在 1 到 100 之间""这个值是加号或减号"……这一章,我们把模式的十八般武艺全部展开。
我们的项目是一台口算机:你输入算式,它立刻算出答案。而且它很宽容——12 + 3 行,12 加 3 也行,7 乘 8、9 除 3 都认。这份"宽容",全靠模式。
18.1 预览:我们要做什么
- 输入算式,比如
12 + 3(运算符用符号或中文都行) - 口算机拆开算式、检查数字、计算
- 数字必须 1~100,不能除以 0,格式要对
运行结果
text
欢迎来到口算机!输入算式,我帮你算!
算式(比如:12 + 3),输入"退出"结束:12 + 3
12 + 3 = 15
算式(比如:12 + 3),输入"退出"结束:7 乘 8
7 乘 8 = 56
算式(比如:12 + 3),输入"退出"结束:9 / 0
数字要在 1 到 100 之间哦。
算式(比如:12 + 3),输入"退出"结束:10 加 90
10 加 90 = 100
算式(比如:12 + 3),输入"退出"结束:150 + 2
数字要在 1 到 100 之间哦。
算式(比如:12 + 3),输入"退出"结束:退出
一共算了 3 道题。再见!这一章你会学到:
| 知识 | 是什么 | 会用在哪儿 |
|---|---|---|
| 切片模式 | 描述"几个元素的列表" | [left, _, right] 拆算式 |
| 元组模式 | 一次拆两个信封 | (Ok(a), Ok(b)) 同时解析 |
| 范围模式 | 值在哪个范围内 | 1..=100 数字检查 |
| 或模式 | 好几个值都走一条路 | "+" | "加" |
@ 绑定 | 匹配到的同时记住它 | 练习三 |
matches! 宏 | "是不是这种形状?" | 练习二 |
18.2 动手做
步骤一:拆开算式
建项目,先写"拆算式"的骨架:
bash
cargo new math_machine
cd math_machine把 src/main.rs 替换成:
rust
use std::io;
fn main() {
println!("欢迎来到口算机!输入算式,我帮你算!");
let mut total = 0;
loop {
println!();
let input = read_input("算式(比如:12 + 3),输入\"退出\"结束:");
if input == "退出" {
break;
}
let parts: Vec<&str> = input.split_whitespace().collect();
println!("拆成了 {:?} 部分", parts);
}
println!("一共算了 {} 道题。再见!", total);
}
fn read_input(prompt: &str) -> String {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}split_whitespace() 是"按空格切成一片片"(第 3 章的 split 家族):"12 + 3" 切成 ["12", "+", "3"]。{:?} 是调试打印(第 4 章的 #[derive(Debug)] 那一手)。
运行试试,输入 12 + 3、7 乘 8,看看拆成什么样。
步骤二:切片模式:拆成三部分
口算机要的算式是"数字 运算符 数字"三部分。怎么检查 parts 正好是三部分,还把三部分拿出来?
第 7 章的笨办法:parts.len() 检查 + parts[0]、parts[2] 取数。第 19 章了,我们有更帅的——切片模式(slice pattern):
rust
let (left, right) = match parts.as_slice() {
[left, _, right] => (left, right),
_ => {
println!("格式不对哦,要像这样:12 + 3");
continue;
}
};parts.as_slice() 把 Vec 变成"一排数据",然后 match 它:
[left, _, right]:描述"正好三个元素"的形状——第一个给left,第二个我不在乎(_,第 5 章老朋友),第三个给right_:不是三部分?兜底,喊"格式不对"
这就是模式的力量:一条 [left, _, right],同时干了两件事——检查"是不是三个" + 把三个取出来。 用 len() 检查再用下标取,要写三行;模式一行搞定,还不会越界。
match 返回 (left, right)——元组(第 7 章),所以 let (left, right) = match ...。left 和 right 现在是 &&str(借来的借来的),打印没事,解析时 Rust 会自动处理。
步骤三:元组模式:一次拆两个信封
left 和 right 是文字,要变成数字(第 2 章 parse)。两个都要解析,还都可能失败——怎么一次检查两个?
元组模式(tuple pattern)出场:
rust
let (left, right) = match (left.parse::<i32>(), right.parse::<i32>()) {
(Ok(left), Ok(right)) => (left, right),
_ => {
println!("数字没看懂哦。");
continue;
}
};(left.parse(), right.parse()) 把两次解析的结果装进一个元组;match 这个元组:
(Ok(left), Ok(right)):两个都成功——把两个数字都取出来(注意这里left/right遮蔽了旧值,变成了i32,第 1 章学过的"阴影遮蔽")_:只要有一个失败(不管是Err),就是"数字没看懂"
一次 match,两个信封,两种结果全处理。元组模式 = 同时拆好几个信封。
步骤四:范围模式:数字要在 1~100
口算机有个规矩:数字只能在 1 到 100 之间。检查"在不在范围内",第 9 章我们会写 if left >= 1 && left <= 100。现在用范围模式(range pattern):
rust
match (left, right) {
(1..=100, 1..=100) => {}
_ => {
println!("数字要在 1 到 100 之间哦。");
continue;
}
}1..=100 是第 2 章的闭区间老朋友,现在它住进了模式里:描述"值在 1 到 100 之间"的形状。(1..=100, 1..=100) 读作:"两个数都在 1 到 100 之间"。
匹配成功(两个都在范围内)就什么都不做({});失败就喊"数字要在 1 到 100 之间"。范围模式让"检查范围"变成一行,比 && 写两遍清爽多了。
步骤五:运算符:或模式
重头戏来了——运算符匹配。它要认识 +、加 两种写法,-、减,*、乘、×……每种符号,多条路,都通向同一个算式:
rust
let result = match parts[1] {
"+" | "加" => left + right,
"-" | "减" => left - right,
"*" | "乘" | "×" => left * right,
"/" | "除" | "÷" => {
if right == 0 {
println!("不能除以 0 哦!");
continue;
}
left / right
}
other => {
println!("不认识这个运算符:{}", other);
continue;
}
};| 或模式(or pattern):"这几个值,都走这条路"。"+" | "加" 读作"加号或者'加'字"。一个分支,管好几种写法,口算机就"宽容"了。
几个细节:
other是绑定模式:把匹配到的值(那个不认识的运算符)绑到变量other,打印出来告诉用户- 除零检查:
right == 0就喊停(还记得第 9 章吗?第 4 步的范围检查已经把 0 挡在门外了——两道防线,双保险) - 每个分支都是"表达式":
left + right是分支的值,整个match的值就是result
步骤六:组装循环
把口算机拼完整——打印结果,计数:
rust
println!("{} {} {} = {}", left, parts[1], right, result);
total += 1;parts[1] 是用户输入的运算符(可能是"加"也可能是"+"——原样打印,让算式看起来就是用户写的那样)。
整个 main.rs 最终形态(第 18.5 节有完整清单)。运行:
运行结果
text
欢迎来到口算机!输入算式,我帮你算!
算式(比如:12 + 3),输入"退出"结束:12 + 3
12 + 3 = 15
算式(比如:12 + 3),输入"退出"结束:7 乘 8
7 乘 8 = 56
算式(比如:12 + 3),输入"退出"结束:9 / 0
数字要在 1 到 100 之间哦。
算式(比如:12 + 3),输入"退出"结束:10 加 90
10 加 90 = 100
算式(比如:12 + 3),输入"退出"结束:abc + 3
数字没看懂哦。
算式(比如:12 + 3),输入"退出"结束:退出
一共算了 3 道题。再见!口算机完工!注意看:整个程序里,检查格式、检查数字、检查范围、选运算符——全靠模式。以前要写一大堆 if,现在几行 match,清楚又安全。
18.3 知识深挖
18.3.1 模式出现在哪些地方?
模式不是 match 的专属。它们藏在 Rust 的各个角落:
rust
let (a, b) = (1, 2); // let 语句:元组模式
let Point { x, y } = point; // let 语句:结构体模式
match value { ... } // match 分支(最熟悉)
if let Some(x) = value { ... } // if let(第 5 章)
while let Some(x) = iter.next() { // while let:边等边循环
// ...
}
for (index, item) in list.iter().enumerate() { // for 循环(第 7 章!)
// ...
}
fn foo((a, b): (i32, i32)) { ... } // 函数参数:元组模式
|(a, b)| a + b // 闭包参数(第 11 章)发现了吗?for (index, pet)、let (a, b)、match 里的 (Ok(a), Ok(b))……全都是模式。 你从第 1 章起就在用模式了,只是今天才给它正名:模式 = "描述值的形状"。
18.3.2 可反驳性:有的形状"保证成立"
模式分两种,区别是会不会匹配失败:
| 不可反驳模式 | 可反驳模式 | |
|---|---|---|
| 会失败吗 | 保证成功 | 可能失败 |
| 例子 | let x = 5、let (a, b) = ...、函数参数 | Some(x)、1..=100、"加" | "+" |
| 能用在 | let、for、函数参数 | match、if let、while let |
规则:let 必须用不可反驳模式(如果可能失败,let 失败时程序就崩了);if let / match 用可反驳模式(它们专门处理"可能失败")。
比如 if let Some(x) = ... 是对的(可能失败,用 if let 接住);但 let Some(x) = ... 编译不过——"可能失败"的形状,不能用在"必须成功"的地方。编译器会直接告诉你,这也是它的安全网。
18.3.3 模式语法全家福
这一章(加上以前的)用到的模式,全家福:
| 模式 | 长什么样 | 干什么 |
|---|---|---|
| 字面量 | "加"、42 | 匹配这个值 |
| 绑定 | x、other | 匹配任何值,并记住它 |
| 或 | "+" | "加" | 几个值都行 |
| 范围 | 1..=100 | 值在这个范围内 |
| 元组 | (Ok(a), Ok(b)) | 同时拆好几个 |
| 切片 | [left, _, right] | 拆"几个元素" |
| 通配符 | _ | 我不管 |
| 省略 | .. | 剩下的我不管(第 14 章 { next, .. }) |
| 绑定 @ | x @ 1..=10 | 匹配范围,同时记住值 |
| 结构体 | Pet { name, .. } | 拆结构体字段(第 14 章) |
还有两个没见到的亲戚:if 守卫(第 20 章预告):
rust
match number {
x if x % 2 == 0 => println!("偶数"),
_ => println!("奇数"),
}if 守卫是"匹配到了,还要过条件"——模式管形状,守卫管条件,合起来更灵活。
18.3.4 matches! 宏:一句话问"是不是?"
有时候你只想问"是不是这种形状?",不想拆东西。matches! 宏专干这个(练习二的主角):
rust
if matches!((left, right), (1..=100, 1..=100)) {
// 两个数都在范围内
}matches!(值, 模式) 返回 bool:匹配上了给 true,没配上给 false。它把"match + 只要 true"压缩成一句话。想"拆开用",用 match;只想"问一句",用 matches!。
18.4 动脑筋练习
练习一:取余运算符
给口算机加"取余"(余数,第 9 章的 %)——符号 %,中文"余"。12 余 5 = 2,7 % 3 = 1。
提示:照抄除法分支的样子(除零检查也要带上!)。
点开看答案
在除法分支的后面加:
rust
"%" | "余" => {
if right == 0 {
println!("不能除以 0 哦!");
continue;
}
left % right
}运行:
text
算式(比如:12 + 3),输入"退出"结束:12 余 5
12 余 5 = 2
算式(比如:12 + 3),输入"退出"结束:7 % 3
7 % 3 = 1新运算符 = 新分支,别的代码一行不用动。口算机的"宽容",靠的是模式的可扩展性:加一种写法,加一个分支。 试着再加"平方"?(提示:left * left,需要设计一下算式格式。)
练习二:matches! 宏
口算机的范围检查用的是"match + 空分支":
rust
match (left, right) {
(1..=100, 1..=100) => {}
_ => { ... }
}用 matches! 宏重写成一句 if。提示:想想"不在范围内"怎么表达(! 取反,第 1 章)。
点开看答案
rust
if !matches!((left, right), (1..=100, 1..=100)) {
println!("数字要在 1 到 100 之间哦。");
continue;
}matches! 问"两个数都在范围内吗?",! 反过来问"不在吗?",在就 continue。原来 4 行,现在 3 行,意思一模一样。
记住分工:要拆出值 → match;只问是不是 → matches!。 口算机的范围检查只是"问一句",所以 matches! 正合适。
练习三:@ 绑定:记住运算符
现在打印结果用的是 parts[1](原始输入)。练习用 @ 绑定:匹配运算符时,用 @ 把它绑到一个变量,然后用这个变量打印。
格式变成 (operator, result) 元组:每个分支都返回"运算符 + 结果"两个值。打印时用 operator 代替 parts[1]。
点开看答案
把运算符的 match 改成:
rust
let (operator, result) = match parts[1] {
operator @ ("+" | "加") => (operator, left + right),
operator @ ("-" | "减") => (operator, left - right),
operator @ ("*" | "乘" | "×") => (operator, left * right),
operator @ ("/" | "除" | "÷") => {
if right == 0 {
println!("不能除以 0 哦!");
continue;
}
(operator, left / right)
}
other => {
println!("不认识这个运算符:{}", other);
continue;
}
};打印处:
rust
println!("{} {} {} = {}", left, operator, right, result);operator @ ("+" | "加") 读作:"匹配加号或'加'字,同时把真正匹配到的那一个绑给 operator"——用户输入 "+" 就绑 "+",输入 "加" 就绑 "加"。@ 是"匹配的同时记住":模式管形状,@ 管记名字。
运行,结果和以前一模一样,但打印用的不是 parts[1],而是模式自己记住的 operator——更"模式化"了。
18.5 完整代码清单
项目结构:
text
math_machine/
├── Cargo.toml
└── src/
└── main.rs文件:Cargo.toml
toml
[package]
name = "math_machine"
version = "0.1.0"
edition = "2024"文件:src/main.rs
rust
use std::io;
fn main() {
println!("欢迎来到口算机!输入算式,我帮你算!");
let mut total = 0;
loop {
println!();
let input = read_input("算式(比如:12 + 3),输入\"退出\"结束:");
if input == "退出" {
break;
}
let parts: Vec<&str> = input.split_whitespace().collect();
let (left, right) = match parts.as_slice() {
[left, _, right] => match (left.parse::<i32>(), right.parse::<i32>()) {
(Ok(left), Ok(right)) => (left, right),
_ => {
println!("数字没看懂哦。");
continue;
}
},
_ => {
println!("格式不对哦,要像这样:12 + 3");
continue;
}
};
match (left, right) {
(1..=100, 1..=100) => {}
_ => {
println!("数字要在 1 到 100 之间哦。");
continue;
}
}
let result = match parts[1] {
"+" | "加" => left + right,
"-" | "减" => left - right,
"*" | "乘" | "×" => left * right,
"/" | "除" | "÷" => {
if right == 0 {
println!("不能除以 0 哦!");
continue;
}
left / right
}
other => {
println!("不认识这个运算符:{}", other);
continue;
}
};
println!("{} {} {} = {}", left, parts[1], right, result);
total += 1;
}
println!("一共算了 {} 道题。再见!", total);
}
fn read_input(prompt: &str) -> String {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}怎么运行:
bash
cd math_machine
cargo run运行检查单:
12 + 3、7 乘 8、20 - 5、9 / 3 都能算出正确答案
中文运算符(加/减/乘/除)和符号(+/−*/÷)都认
数字超出 1~100,提示"数字要在 1 到 100 之间"
输入 abc + 3 或 12 + 之类,都有礼貌的提示,不崩溃
输入 退出,报出总题数,正常结束
把 [left, _, right] 改成 [left, right],看编译器怎么提示"模式没覆盖"