外观
第 2 章 猜数字游戏
小螃蟹说
上一章我们用猜拳热身,这一章来点更烧脑的!我偷偷在心里想一个 1 到 100 之间的数字,你负责猜,我负责告诉你猜大了还是猜小了。猜得越快,说明你的运气和头脑越棒!
猜数字是世界闻名的入门小游戏,几乎所有编程书的第一个项目都是它。不过我们已经比别的书多会了一个猜拳游戏啦。这一章你会发现:很多知识是上一章的"老朋友",真正的新朋友只有四个——match、parse、Ordering 和 continue。
2.1 预览:我们要做什么
猜数字游戏规则:
- 小螃蟹在心里想一个 1 到 100 之间的秘密数字
- 你输入一个数字猜一猜
- 猜小了,提示"太小了";猜大了,提示"太大了"
- 猜对,游戏结束,告诉你一共猜了几次
运行结果
text
猜数字游戏!
我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!
请输入你的猜测(1~100):
50
太小了!再往大猜猜。
请输入你的猜测(1~100):
75
太大了!再往小猜猜。
请输入你的猜测(1~100):
abc
这不是一个数字哦,请重新输入。
请输入你的猜测(1~100):
63
猜对了!秘密数字就是 63!
你一共猜了 3 次。小贴士
看,输入 abc 时游戏没有崩溃,而是礼貌地请你重新输入。上一章的猜拳游戏也有这样的礼貌,还记得我们是怎么做的吗?
这一章你会学到:
| 知识 | 是什么 | 会用在哪儿 |
|---|---|---|
match | 分岔路口的牌子,按情况走不同的路 | 判断猜大了、猜小了还是猜对 |
parse | 把一种东西变成另一种(类型转换) | 把文字 "50" 变成数字 50 |
Ordering | 比较的结果:小、大、相等 | cmp 方法比较两个数字 |
continue | 跳过这一圈,直接开始下一圈 | 输入不是数字时重新输入 |
2.2 动手做
步骤一:新建项目
和上一章一样,先让 Cargo 盖小屋:
bash
cargo new guessing_game
cd guessing_gameguessing_game 就是"猜数字游戏"的意思。
步骤二:想一个秘密数字
先在 Cargo.toml 里请 rand 帮忙(上一章学过,这次抄作业就行):
toml
[dependencies]
rand = "0.9"然后打开 src/main.rs,把 Cargo 送的 Hello world 替换成下面这样:
rust
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
}注意 1..=100 和上一章的 0..3 长得有点像,但中间多了个等号。这是闭区间(closed range):..= 是"到这个数为止,包括它",所以秘密数字有可能是 1,也有可能是 100。上一章的 0..3 是不包括 3 的(3 这个编号不存在),所以叫半开区间。
步骤三:猜一次试试
现在让你猜一次。在 main 函数里加代码:
rust
use std::io;
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let guess: u32 = input.trim().parse().expect("这不是一个数字!");
println!("你猜的是:{}", guess);
}注意别漏了第一行 use std::io;。
运行它,猜一个数试试:
运行结果
text
猜数字游戏!
我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!
50
你猜的是:50这一行代码里有个新朋友:
rust
let guess: u32 = input.trim().parse().expect("这不是一个数字!");parse 的意思是解析(parse),把一种东西变成另一种。我们输入的文字 "50" 装在一个 String 里,parse() 把它解析成一个真正的数字 50。看变量名旁边的 : u32,这是在告诉 Rust:"我要的是 u32 类型的数字!"没有这个声明,Rust 就不知道你想把文字变成什么类型的数字,会急得跳脚。
小贴士
parse 是个挑食的家伙:给它 "50" 它开开心心变成 50,给它 "abc" 它就翻脸。翻脸时会发生什么?试一下——把 50 换成 abc 运行,看看程序怎么"原地去世"的。下一节我们会教它礼貌一点。
步骤四:告诉你是大是小
光知道自己猜了什么可不够,得让小螃蟹告诉你猜对没有。把"打印你猜的是什么"那一行删掉,换成比较大小:
rust
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let guess: u32 = input.trim().parse().expect("这不是一个数字!");
match guess.cmp(&secret) {
Ordering::Less => println!("太小了!再往大猜猜。"),
Ordering::Greater => println!("太大了!再往小猜猜。"),
Ordering::Equal => println!("猜对了!秘密数字就是 {}!", secret),
}
}guess.cmp(&secret) 是让 guess 和 secret 比大小。比完的结果,不是"true 或 false"这么简单,而是三选一:Ordering::Less(小了)、Ordering::Greater(大了)、Ordering::Equal(相等)。Ordering 是"比较的结果"。
那怎么根据结果走不同的路呢?这就轮到新朋友 match 登场了。match 像分岔路口的牌子:拿着一个值,挨个和牌子上的字对——对上了,就沿着那条路走。走完一条路自动停下,不会顺便把别的路也走了。
小贴士
还记得我们上一章用 if / else if 做过类似的事吗?match 和它的区别是:牌子必须挂全。Rust 要求 match 把每一种可能都列出来,少一个它都不让编译——这是它的安全网,保证每条路都有人管。
运行试试:
运行结果
text
猜数字游戏!
我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!
75
太大了!再往小猜猜。步骤五:猜对为止
现在游戏只让你猜一次。把它改成一个能一直玩下去的游戏:猜错就重来,猜对就庆祝。把 main 函数替换成下面这样:
rust
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
let mut attempts = 0;
loop {
println!();
println!("请输入你的猜测(1~100):");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let guess: u32 = input.trim().parse().expect("这不是一个数字!");
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("太小了!再往大猜猜。"),
Ordering::Greater => println!("太大了!再往小猜猜。"),
Ordering::Equal => {
println!("猜对了!秘密数字就是 {}!", secret);
println!("你一共猜了 {} 次。", attempts);
break;
}
}
}
}两个新东西:
attempts 计数器。每猜一次 attempts += 1 自己加一。mut 老朋友了——要改它,就得给它钥匙。
break 换了个地方用。上一章 break 在 if 里;这一章它在 match 的一个分支里。不管在哪儿,它都是同一个意思:"跳出循环!"猜对时,游戏先庆祝,然后 break 跳出圈,程序结束。
小贴士
loop 每次都重新"读一行输入"——注意 let mut input = String::new(); 在循环里面。每一圈都造一个新的文字盒子,这样就不会把上一轮的旧输入留在里面。
步骤六:输错了别慌
还记得步骤三里那个"原地去世"的 expect 吗?现在游戏循环了,更要命的来了:你输入 abc,parse 翻脸,expect 直接让游戏崩溃——好不容易猜对的次数全没了!
我们换成有礼貌的版本:把这一步:
rust
let guess: u32 = input.trim().parse().expect("这不是一个数字!");换成这样:
rust
let guess: u32 = match input.trim().parse() {
Ok(number) => number,
Err(_) => {
println!("这不是一个数字哦,请重新输入。");
continue;
}
};这里藏着今天最重要的知识点。parse 的成功与否,装在一个叫 Result 的信封里:
- 成功 → 信封上写着
Ok,里面装着转换好的数字 - 失败 → 信封上写着
Err,里面装着失败的原因
我们用 match 拆信封:Ok(number) 就把数字取出来用;Err(_) 说明失败了,打印一句提醒,然后 continue——跳过这一圈,直接开始下一圈。和 break(出圈)是亲戚,一个是"这圈不要了",一个是"整个不要了"。
Err(_) 里的 _ 是"我不在乎"的意思:失败的原因是什么,我才懒得管,反正都是重新输入。
现在整个 main.rs 是最终形态:
rust
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
let mut attempts = 0;
loop {
println!();
println!("请输入你的猜测(1~100):");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let guess: u32 = match input.trim().parse() {
Ok(number) => number,
Err(_) => {
println!("这不是一个数字哦,请重新输入。");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("太小了!再往大猜猜。"),
Ordering::Greater => println!("太大了!再往小猜猜。"),
Ordering::Equal => {
println!("猜对了!秘密数字就是 {}!", secret);
println!("你一共猜了 {} 次。", attempts);
break;
}
}
}
}运行,开始猜吧!想玩得聪明,用"二分法":先猜 50,根据提示再猜 25 或 75……理论上最多 7 次就能猜到!试试点评一下自己是不是聪明蛋:
运行结果
text
猜数字游戏!
我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!
请输入你的猜测(1~100):
50
太小了!再往大猜猜。
请输入你的猜测(1~100):
75
太大了!再往小猜猜。
请输入你的猜测(1~100):
63
猜对了!秘密数字就是 63!
你一共猜了 3 次。2.3 知识深挖
2.3.1 match:分岔路口的牌子
match 是 Rust 最常用的控制流,以后几乎每个程序都会用到它。它的样子:
rust
match guess.cmp(&secret) {
Ordering::Less => println!("太小了!再往大猜猜。"),
Ordering::Greater => println!("太大了!再往小猜猜。"),
Ordering::Equal => {
println!("猜对了!秘密数字就是 {}!", secret);
println!("你一共猜了 {} 次。", attempts);
break;
}
}解剖一下:
match后面跟一个值,这是"要判断的东西"- 大括号里,每一行是一个分支(arm):左边是"牌子"(模式),右边
=>后面是"要做的路" - 拿着值从左往右挨个对牌子,对上了就走那条路,然后整个 match 结束
=> 右边可以是一行,也可以是一个大括号块。分支里想放多少行代码都行,只要包在大括号里。
有两个和 if 不一样的地方,一定要记住:
- match 必须覆盖所有情况。
Ordering只有三种可能,少列一种,编译器直接报错:"你漏了!"这是故意的——编译器逼你想全,免得程序运行到一半不知所措。 - match 不需要 break。它不像
if/else if那样要小心别"溜进"下一个分支,它走完一条路就自动停。
提前预告
Ordering::Less 和 Ordering::Equal 里那个 :: 你见过——String::new() 也是它。但 Less、Equal 和 String 不是一回事:String 是类型,Less 是枚举(enum)的一种变体。枚举是什么?第 5 章动物叫声模拟器,我们用它把小猫小狗奶牛全部装进一个类型里——那是 match 大显身手的主场!
2.3.2 类型转换:把文字变成数字
电脑里,"5"和 5 完全是两码事:一个是文字,一个是数字。文字不能做算术,数字不能打印成句子。类型转换就是在它们之间搭桥。
parse 就是一座桥:
rust
input.trim().parse()parse 是把文字变成数字的桥,但有个细节:它可能成功,也可能失败("abc" 怎么变数字?变不了)。所以它不直接给你数字,而是给你一个信封——Result。
Result 是 Rust 处理"可能出错的事情"的专用信封,只有两种可能:
| 信封 | 意思 | 里面装着 |
|---|---|---|
Ok(值) | 成功了 | 真正的结果 |
Err(错误) | 失败了 | 失败的原因 |
我们的两种拆法:
rust
// 粗鲁版:失败了就崩溃
let guess: u32 = input.trim().parse().expect("这不是一个数字!");
// 礼貌版:失败了就自己处理
let guess: u32 = match input.trim().parse() {
Ok(number) => number,
Err(_) => {
println!("这不是一个数字哦,请重新输入。");
continue;
}
};.expect("……") 像一扇挂着"如有损坏,概不负责"的门:成功了放行,失败了直接摔门(程序崩溃)。match 拆信封则像细心的工作人员:Ok 收下包裹,Err 登记一下慢慢处理。
小贴士
expect 崩溃时,会把括号里的话打印出来帮你定位问题。它适合"这里绝对不该出错"的地方。match 适合"可能会出错、要好好处理"的地方。以后我们还会学到更专业的错误处理(第 8 章),现在这两个够用了。
2.3.3 Ordering:比较的结果
guess.cmp(&secret) 念作"猜的和秘密数字比较"。它的结果不是"是/否",而是三种排序关系(ordering):小于、大于、相等。
这就是 Ordering 枚举的全部家当:
rust
enum Ordering {
Less, // 小于
Greater, // 大于
Equal, // 等于
}(这是它真实模样的简化版,但意思一模一样。)
有人会问:为什么不用 if guess < secret?因为"比大小"这件事,小于、大于、等于三种结果,新手用 if/else 很容易漏一种——漏了不等于,游戏就会在你猜对时什么都不说。match 的安全网在这里又一次生效:枚举的变体必须全列,漏一个编译不过。编译器替你想全了。
2.3.4 continue:跳过这一圈
loop 是转圈圈,圈圈里有两个"操控杆":
break:整个圈不要了,跳出去(上一章学过)continue:这一圈剩下的路不走了,立刻开始下一圈
rust
Err(_) => {
println!("这不是一个数字哦,请重新输入。");
continue; // 这一圈到此为止,下一圈再问一次
}注意 attempts += 1 在 match 的下面。输入 abc 时 continue 直接跳过了它——所以乱输不会被记次数,很公平!
2.3.5 复习课:老朋友与新朋友
这一章用到的知识,老朋友不少,新朋友四个:
| 知识 | 老朋友? | 什么时候学的新东西 |
|---|---|---|
let / mut 变量 | ✅ 第 1 章 | attempts 计数器 |
String / stdin | ✅ 第 1 章 | 没有,熟练了而已 |
loop / break | ✅ 第 1 章 | break 住进了 match 分支 |
if / else if | ✅ 第 1 章 | 没出现!被 match 顶班了 |
rand crate | ✅ 第 1 章 | 1..=100 闭区间 |
match | 🆕 | 分岔路口,必须覆盖所有情况 |
parse / Result | 🆕 | 文字变数字,Ok / Err 信封 |
Ordering / cmp | 🆕 | 比较结果:小、大、相等 |
continue | 🆕 | 跳过这一圈 |
发现了吗?很多程序 = 老朋友反复用 + 新朋友偶尔来。编程就是这样:学得越多,越会发现"好像都在哪里见过"。
2.4 动脑筋练习
练习一:缩小难度
把秘密数字的范围改成 1 到 50。这一章我们提过常量,试着用一个 const MAX: u32 = 50; 让范围只写一次——以后想改成 1 到 10,只动一行。
点开看答案
分三步。
第一步,在 main 函数里加常量:
rust
const MAX: u32 = 50;
let secret = rand::rng().random_range(1..=MAX);第二步,把 1..=100 改成 1..=MAX(就是上面那行,已经写好了)。
第三步,广告词也要改,不然游戏玩着玩着就露馅:
rust
println!("我心里想了一个 1 到 50 之间的秘密数字,你猜猜看!");const 的规矩:名字全大写,中间用下划线隔开(比如 MAX_NUMBER),声明时就要给值,之后谁都不能改它。广告词里那句"1 到 50"还是写死的——想把它也变成常量?可以再定义一个 const 存 MAX 的数字,然后用 {} 填进去。试试看!
练习二:把捣乱的挡在门外
现在输入 0 或 101,游戏会告诉你"太小了/太大了"。这不礼貌——它们根本不在游戏范围里。加一个检查:数字不在 1 到 100 之间时,提醒一句,重新输入,而且不算次数。
提示:想想 continue 应该放在哪儿,才不会让 attempts 白加一。
点开看答案
在 attempts += 1; 的上面,加一个 if:
rust
if guess < 1 || guess > 100 {
println!("这个数字不在 1 到 100 里哦。");
continue;
}
attempts += 1;顺序很关键:continue 会跳过 attempts += 1,所以捣乱的数字不会计次。如果你把 if 放在 attempts += 1 下面,捣乱的数字就会被记一次——试试看,能发现这个区别吗?
练习三:给猜谜高手颁奖
猜对时,根据猜的次数颁发不同的"奖状":
- 第 1 次就猜中:"一次就猜中?!你是不是偷看了我的心!"
- 5 次以内:"你真是个猜数字小天才!"
- 10 次以内:"厉害,猜得又快又准!"
- 10 次以上:"坚持不懈,终于猜中了!"
提示:奖状要贴在"猜对了"的分支里,break 的前面。会用到第 1 章学的 if / else if 链。
点开看答案
把 Ordering::Equal 分支改成:
rust
Ordering::Equal => {
println!("猜对了!秘密数字就是 {}!", secret);
if attempts == 1 {
println!("一次就猜中?!你是不是偷看了我的心!");
} else if attempts <= 5 {
println!("你真是个猜数字小天才!");
} else if attempts <= 10 {
println!("厉害,猜得又快又准!");
} else {
println!("坚持不懈,终于猜中了!");
}
println!("你一共猜了 {} 次。", attempts);
break;
}注意 if 判断的顺序:先问"是不是 1 次",再问"是不是 5 次以内"……顺序反了会怎样?如果先问 attempts <= 10,那 1 次猜中的也会拿到"又快又准"的奖状。判断顺序就是程序的脾气,一定要想好。
2.5 完整代码清单
项目结构:
text
guessing_game/
├── Cargo.toml
└── src/
└── main.rs文件:Cargo.toml
toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9"文件:src/main.rs
rust
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("猜数字游戏!");
println!("我心里想了一个 1 到 100 之间的秘密数字,你猜猜看!");
let secret = rand::rng().random_range(1..=100);
let mut attempts = 0;
loop {
println!();
println!("请输入你的猜测(1~100):");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let guess: u32 = match input.trim().parse() {
Ok(number) => number,
Err(_) => {
println!("这不是一个数字哦,请重新输入。");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("太小了!再往大猜猜。"),
Ordering::Greater => println!("太大了!再往小猜猜。"),
Ordering::Equal => {
println!("猜对了!秘密数字就是 {}!", secret);
println!("你一共猜了 {} 次。", attempts);
break;
}
}
}
}怎么运行:
bash
cd guessing_game
cargo run运行检查单:
输入数字,会提示太大还是太小
输入 abc 之类的乱码,游戏不崩溃,请你重新输入
乱输的次数不算进 attempts
猜对时显示秘密数字和总次数
猜对后游戏结束,窗口回到终端