猜数游戏入门
约 2447 字大约 8 分钟
2026-02-02
猜数游戏
- 开始:程序启动。
- 生成随机数:程序在1到100之间生成一个随机整数,并将其存储。提示用户输入猜测:程序提示用户输入一个数字,作为对随机数的猜测。
- 判断猜测:
- 猜对了:如果用户的猜测与随机数相等,程序输出祝贺信息并结束游戏。
- 猜错了:如果用户的猜测与随机数不相等,程序会判断猜测是太大了还是 太小,并给出相应的提示。
- 回到步骤3:如果用户猜错了,程序会再次提示用户输入猜测, 重复步骤4。
涉及知识点
- 随机数引入
- 用户输入
- 循环判断
用 0 基础写出第一个 Rust 小游戏:猜数字(从 println! 到 match、loop、随机数)
目标:你将从零开始写出一个完整可运行的“猜数字”命令行小游戏,并在过程中理解每一段代码:println!、输入读取、字符串转数字、Result、match、随机数、比较、循环与退出。 本文解释遵循 Rust 官方书籍/官方标准库文档的概念与术语(如:宏、表达式、枚举、Result、match 表达式、可变引用等)。
你最终会写出什么?
运行程序后,终端会不断提示你输入数字:
输入太小:提示 Too small
输入太大:提示 Too big
输入正确:结束游戏
(为了教学清晰,本文会把代码分成多步,每一步都能 cargo run 跑通。)
0. 准备工作:创建项目 + 添加依赖
0.1 创建 Rust 项目
在终端执行:
cargo new guess_number
cd guess_numbercargo 是 Rust 的构建工具与包管理器。它会生成一个可运行的项目结构,并创建 src/main.rs。
0.2 添加随机数库 rand
Rust 标准库不提供随机数生成器,所以我们使用常用的第三方 crate:rand。
cargo add rand执行后,你会在 Cargo.toml 看到:
[dependencies]
rand = "..."说明:你贴的代码使用了 rand::random_range(...),这是 rand 新版本提供的函数;很多旧教程(包括早期示例)会使用 thread_rng().gen_range(...)。本文会给出两种写法的说明,并优先给出更“通用、遇到坑更少”的方式。
1. 第一步:学会输出(println!)
打开 src/main.rs,先写最小程序:
fn main() {
println!("begin guess number");
}1.1 println! 是什么?
println! 是 宏(macro),不是普通函数。
Rust 中宏名称后面带 !,宏在编译期展开,可以接收灵活的参数形式。
你可以这样输出变量:
let x = 10;
println!("x = {}", x);这里 {} 是格式化占位符,把变量以默认格式插入字符串。
2. 第二步:读取用户输入(std::io、stdin、read_line)
现在我们让用户在终端输入一行内容。
use std::io;
fn main() {
println!("input guess number:");
let mut guess_str = String::new();
io::stdin()
.read_line(&mut guess_str)
.expect("Failed to read line");
println!("you typed: {}", guess_str);
}2.1 use std::io; 是什么?
std::io 是 Rust 标准库里的 模块,提供输入输出相关的类型与函数。
use 的作用是把路径引入当前作用域,便于写 io::stdin(),而不是每次写全路径 std::io::stdin()。
2.2 String::new() 是什么?
String 是标准库提供的“可增长、拥有所有权”的字符串类型(内容在堆上)。
String::new() 创建一个空字符串。
2.3 为什么要 mut?
read_line 会把用户输入写进 guess_str,这意味着 guess_str 必须是可变的:let mut guess_str = ...
2.4 &mut guess_str 是什么?
&mut 表示 可变引用(mutable reference)。
你把 guess_str 的可变引用“借给” read_line,允许它在不拿走所有权的情况下修改字符串内容。
2.5 read_line(...).expect(...) 做了什么?
read_line 会返回一个 Result:
成功:Ok(读取到的字节数)
失败:Err(错误信息)
expect("...") 的行为是:
如果是 Ok(...):继续执行
如果是 Err(...):直接让程序崩溃并显示你给的提示文字
新手阶段用 expect 很常见:因为它让你先把流程跑通,后面再学更严谨的错误处理。
3. 第三步:把字符串转换成数字(trim + parse + match)
真实游戏里,我们需要把输入从字符串变成数字。
3.1 为什么要 trim()?
read_line 通常会把换行符也读进来,例如你输入 42,字符串可能是 "42\n"。 trim() 会去掉首尾空白(包含换行),得到更干净的内容。
3.2 parse() 返回什么?
parse() 会把字符串解析成某个类型,但它不是“必然成功”的,所以它返回 Result:
Ok(解析后的值)
Err(解析失败的错误)
3.3 match 是什么?它会“返回”什么?
match 在 Rust 里是 match 表达式。 表达式的关键点:它会求值并产生一个结果。
因此你可以写:
let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num,
Err(_) => 0,
};上面这段的含义是:
如果解析成功,就把 num 作为整个 match 的结果
否则返回 0
在猜数字游戏中,我们不希望解析失败时“默认变成 0”,更合理的是“让用户重新输入”。因此使用 continue(进入下一轮循环)更合适:
let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};Ok(num) / Err(_):分别匹配 Result 的两个变体(枚举的 variants)
_:通配符,表示“我不关心这个值是什么”
continue:立刻结束本次循环,进入下一次循环(后面会讲 loop)
4. 第四步:生成随机数(rand + 范围 Range)
现在我们需要一个 1~100 的随机整数。
4.1 范围 1..101 是什么意思?
1..101 表示从 1 到 100(包含 1,不包含 101)
这符合猜数字常见设定:1~100
4.2 rand 的两种常见写法(看你用的是哪种 API)
写法 A:更通用(很多教程/示例都兼容)
use rand::Rng;
let random_num: i32 = rand::thread_rng().gen_range(1..101);解释:
rand::thread_rng():获取线程本地随机数生成器
gen_range(1..101):在给定范围生成随机值
use rand::Rng;:因为 gen_range 是 Rng trait 提供的方法,需要把 trait 引入作用域
写法 B:新版提供的便捷函数
let random_num: i32 = rand::random_range(1..101);解释:
这是 rand 新版提供的简写形式
实际底层等价于“拿到线程本地 RNG,再从范围生成随机值”的思路
如果你在编译时遇到 “找不到 random_range” 或 feature 相关报错,请改用写法 A(更稳妥),因为它在各种环境/教程版本里更常见。
5. 第五步:比较大小(cmp + Ordering + match)
我们要判断“猜小了/猜大了/猜对了”。
Rust 标准库里常见写法是:
use std::cmp::Ordering;
match guess_num.cmp(&random_num) {
Ordering::Less => println!("Too small number"),
Ordering::Greater => println!("Too big number"),
Ordering::Equal => println!("You win!"),
}5.1 cmp 是什么?
cmp 是一个比较方法:比较两个值的大小关系,并返回 Ordering。
5.2 Ordering 是什么类型?
Ordering 是一个 枚举(enum),只有三种可能:
Less:小于
Equal:等于
Greater:大于
5.3 为什么 cmp(&random_num) 要加 &?
cmp 的参数是“另一个值的引用”。因此传 &random_num(对随机数的引用)符合方法签名要求。
6. 第六步:循环直到猜对(loop + continue + break)
最后把所有东西串起来:反复读输入 → 解析 → 比较 → 给反馈 → 猜对就退出。
完整版代码(教学注释版)
下面给出一份“清晰、可运行、注释够新手理解”的最终版本。我使用更通用的 rand 写法 A(更不容易被版本差异坑到)。你也可以替换成 random_range。
use rand::Rng; // 引入 Rng trait,才能用 gen_range
use std::cmp::Ordering; // 引入 Ordering 枚举,用于比较结果
use std::io; // 标准输入输出模块
fn main() {
println!("begin guess number");
// 生成 1..101 范围内的随机数(实际是 1~100)
let random_num: i32 = rand::thread_rng().gen_range(1..101);
// loop:无限循环,直到遇到 break
loop {
println!("input guess number");
// 用 String 保存用户输入
let mut guess_str = String::new();
// 从标准输入读取一行,写入 guess_str
io::stdin()
.read_line(&mut guess_str)
.expect("Failed to read line");
// trim 去掉首尾空白(包括换行符),parse 尝试解析成 i32
// parse 返回 Result,所以用 match 分情况处理
let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num, // 成功:拿到解析出的数字
Err(_) => continue, // 失败:不是数字,重新开始下一轮循环
};
// cmp 比较两个数,返回 Ordering(Less/Greater/Equal)
match guess_num.cmp(&random_num) {
Ordering::Less => println!("Too small number"),
Ordering::Greater => println!("Too big number"),
Ordering::Equal => {
println!("Correct! Game over.");
break; // 猜对了:退出 loop
}
}
}
}常见新手问题(非常关键)
1) 为什么 match 看起来像 switch,但还能赋值给变量?
因为 Rust 的 match 是表达式:它会产生一个值,所以能写 let x = match ... { ... };。
2) 为什么 parse 不直接给数字,还要 Ok/Err?
因为字符串不一定是合法数字(比如输入 abc)。Rust 用 Result 明确表达“可能失败”,这是一种强类型的错误处理方式。
3) 为什么每次循环都 String::new()?
因为每轮我们都要读一行新输入;新建一个空 String 最直观。你也可以复用同一个 String,但要记得清空内容(新手先不急)。
练习扩展(从入门到进阶的自然路线)
下面这些练习能让你对本例的每个概念更扎实:
输入提示不换行:把 println! 改成 print!,并在读输入前 flush 一下(理解缓冲)。
输入错误给提示:当 Err(_) 时打印一句 “Please input a number”。
统计猜测次数:加一个 attempts 计数器,猜对时输出你猜了几次。
限制输入范围:如果输入 <1 或 >100,提示“范围必须是 1~100”。
贡献者
flycodeu
版权所有
版权归属:flycodeu
