一、项目概述
1.1 为什么选择“猜数字”?
“猜数字”是编程入门中非常经典的一个项目。它看似简单,却能很好地展示:
- 输入输出 (I/O):提示用户输入并读取内容。
- 随机数:每次运行生成一个随机值,保证游戏的多样性。
- 逻辑判断与控制流:比较大小并给出提示。
- 错误处理:当用户输入不合法内容时,我们如何应对?
- 循环:允许玩家多次猜测,直到猜对为止。
这些功能组合起来,就能覆盖 Rust 中相当多的常用特性和写法,非常适合用来练手和深入理解语言机制。
1.2 本文内容预览
- 环境准备与项目初始化:安装 Rust,创建新项目结构。
- 用户输入与输出:使用 Rust 标准库
std::io
读写命令行数据。 - 随机数生成:学习如何在
Cargo.toml
中添加依赖并使用社区库rand
。 - 字符串解析与比较:将输入的字符串转为数字,并比较与随机数的大小。
- 循环与错误处理:通过
loop
允许多次猜测,使用match
处理各种错误场景。 - 总结与扩展:提供改进思路,例如多模式输入、添加回合数等。
在这个过程中,我们将看到 Rust 是如何在编译期就帮助我们杜绝许多潜在错误,并且通过 Cargo 让依赖管理和构建变得简单高效。
二、准备工作:安装 Rust 与熟悉 Cargo
2.1 安装 Rust
Rust 官方推荐用 rustup 安装与管理版本。简单来说,终端里执行一行安装脚本即可完成大部分操作。安装完成后,可用下列命令确认版本:
rustc --version
cargo --version
如果能正确显示版本号,说明你已经拥有了编写和构建 Rust 程序所需的环境。
2.2 初步认识 Cargo
Cargo 是 Rust 自带的包管理工具兼构建系统,为我们提供以下功能:
- 快速初始化新的 Rust 项目骨架
- 管理第三方库(crate)依赖
- 编译、运行、测试、发布项目
在本教程中,Cargo 将始终贯穿项目的整个生命周期,帮助我们简化琐碎操作。
三、新建“猜数字”项目
3.1 使用 Cargo 创建项目
在你想要放置项目的目录下,执行:
cargo new guessing_game
cd guessing_game
这会生成一个 guessing_game
文件夹,内部结构如下:
guessing_game
├── Cargo.toml
└── src
└── main.rs
Cargo.toml
:项目的配置文件,包含依赖版本、项目元数据等。src/main.rs
:Rust 程序入口。
3.2 默认示例运行
src/main.rs
里默认写着 “Hello, world!” 测试代码。若你执行:
cargo run
就会看到编译并立即运行,屏幕上输出“Hello, world!”。这说明我们的项目骨架和工具链都已就绪。
四、读入用户输入并输出到控制台
4.1 引入 std::io
猜数字游戏的第一步是让用户在命令行输入他们的猜测。Rust 标准库中负责读写终端输入输出的主要模块是 std::io
,我们需要在 main.rs
顶部写:
use std::io;
4.2 读入并存储到字符串
让我们写一段能够提示用户并读入其输入的代码。替换 main.rs
的内容为:
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
逐行分析:
println!("Guess the number!")
:打印游戏开场信息。println!("Please input your guess.")
:提示玩家输入。let mut guess = String::new();
:创建一个可变字符串guess
用于存储用户输入。io::stdin().read_line(&mut guess)
:从标准输入读取一行文本并附加到guess
。.expect("Failed to read line")
:如果读取失败,就打印错误并退出程序。println!("You guessed: {guess}")
:将玩家输入的内容原样输出,验证读取结果。
运行 cargo run
,输入一些内容后回车,程序会将你的输入回显出来,这说明 I/O 已正确工作。
五、生成随机数:使用 rand
crate
5.1 在 Cargo.toml
中添加依赖
Rust 标准库并未内置随机数生成器,需要借助第三方库(crate)。最常用的库是 rand。
打开 guessing_game
文件夹下的 Cargo.toml
,在文件末尾添加:
[dependencies]
rand = "0.8.5"
保存后,执行:
cargo build
这时 Cargo 会自动下载并编译 rand
及其依赖。初次下载可能稍慢,但后续都会利用缓存,加快构建速度。
5.2 在代码中使用 rand
在 main.rs
顶部增加:
use rand::Rng;
然后在 main
函数中生成一个 1~100 的随机整数:
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}"); // 测试用
println!("Please input your guess.");
// ...
}
rand::thread_rng()
:获得一个基于当前线程的随机数生成器。.gen_range(1..=100)
:指定生成区间是[1, 100]
。
再次运行 cargo run
可以看到,每次执行都会输出一个不同的随机数。(调试完成后可删除这行输出,避免剧透。)
六、解析并比较用户猜测与神秘数
6.1 将字符串转为数字
用户的输入在 Rust 中被读取到一个 String
里,如果想要比较,就必须转成数字。最简单的方式是:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
guess.trim()
:去除首尾空白字符(含换行)。.parse()
:将字符串解析成指定数值类型,这里是u32
。.expect("...")
:若解析失败,打印错误并退出。
6.2 使用 std::cmp::Ordering
和 match
进行比较
Rust 的比较函数 cmp
会返回一个 Ordering
枚举,可能是 Less
、Greater
或 Equal
。我们用 match
对应不同情况进行处理:
use std::cmp::Ordering;
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
把这些放进 main.rs
里,就能初步实现一次猜测的逻辑:
fn main() {
use std::io;
use rand::Rng;
use std::cmp::Ordering;
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
现在可以猜一次,但猜错后程序就直接结束。我们需要让玩家多次猜测,直到猜中为止。
七、允许多次猜测:使用 loop
搭配 break
7.1 无限循环
Rust 提供了 loop
关键字,可用来创建一个无限循环。我们只要在合适时机用 break
跳出循环即可。修改代码如下:
fn main() {
use std::io;
use rand::Rng;
use std::cmp::Ordering;
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break; // 猜对就退出循环
}
}
}
}
程序运行后,当你输入数字并与“神秘数”不匹配时,会再次提示输入;只有在 guess == secret_number
时打印“You win!” 并结束。
7.2 更友好的错误处理
目前,若用户输入非数字(例如 “abc”),expect("Please type a number!")
就会直接崩溃退出,这并不友好。想继续游戏,就得显式地通过 match
来处理可能出现的解析错误:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Please type a valid number!");
continue; // 直接跳过本次循环迭代
}
};
如此,当玩家输入非法内容时,程序会提示重试而不崩溃。将此逻辑整合到整体代码中:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
// 改用 match 来处理 parse 的返回
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Please type a valid number!");
continue;
}
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break; // 跳出循环
}
}
}
}
删除调试用的打印神秘数行,我们就完成了最核心、最简洁的猜数字游戏!
八、深入理解:Rust 关键机制与编译特性
-
所有权与借用 (Ownership & Borrowing)
- 在
read_line(&mut guess)
里,我们传的是&mut guess
(一个可变引用),保证同时只有一个对该数据的可变引用,从而避免了并发读写时的竞争。 - Rust 编译器会在编译期自动帮你检查借用规则,如果出现违规借用,代码就无法通过编译。
- 在
-
Result 枚举与错误处理
- 像
read_line
、parse
这样的函数都返回一个Result
。它可能是Ok(T)
表示成功,也可能是Err(E)
表示失败,并携带错误信息。 - Rust 强制在编译期显式处理错误,这能让代码更健壮,减少运行期意外崩溃。
- 像
-
match 表达式
match
要求你列出所有可能的匹配分支,编译器会检查是否有遗漏;这也符合 Rust“让潜在错误在编译期就暴露”的设计理念。- 例如,我们用
match guess.cmp(&secret_number)
彻底覆盖了Less
、Greater
、Equal
三个场景。
-
Cargo 编译缓存
- 在第一次编译或添加新依赖时,Cargo 可能要下载并编译更多内容。但只要你后续没变动这些依赖,Cargo 不会重复下载编译,执行
cargo build
或cargo run
都会非常高效。
- 在第一次编译或添加新依赖时,Cargo 可能要下载并编译更多内容。但只要你后续没变动这些依赖,Cargo 不会重复下载编译,执行
-
可移植性与跨平台
- Rust 标准库本身就是跨平台的,如果你在某平台编译并生成二进制文件,在同样架构、操作系统的机器上通常都能直接运行。这让部署工作变得简单许多。
九、项目运行演示
假设我们已经全部代码写好且去掉了 secret_number
的打印,现在执行 cargo run
,会如下所示:
Guess the number!
Please input your guess.
30
You guessed: 30
Too small!
Please input your guess.
abc
Please type a valid number!
Please input your guess.
80
You guessed: 80
Too big!
Please input your guess.
66
You guessed: 66
You win!
- 当输入数字不合法时,程序不会崩溃,而是继续让你重试。
- “Too small!” 或 “Too big!” 提示可以帮助玩家逐步逼近答案,直到猜中后结束。
这就是我们想要的完整小游戏体验!
十、可能的扩展与改进
-
自定义退出命令
- 在玩家想要提前退出时,可以输入 “q” 或 “quit”。实现上,可以在
parse()
前先判断guess.trim()
是否等于"q"
等,再决定是退出还是继续。
- 在玩家想要提前退出时,可以输入 “q” 或 “quit”。实现上,可以在
-
增加回合数统计
- 可以在循环外声明一个
let mut attempts = 0;
,每猜一次就attempts += 1;
。猜中后告诉玩家“你总共猜了 x 次”。
- 可以在循环外声明一个
-
多重难度
- 在游戏开始时让用户选择难度,比如简单难度(150)和困难难度(11000)。根据选择,动态调整
gen_range
的范围。
- 在游戏开始时让用户选择难度,比如简单难度(150)和困难难度(11000)。根据选择,动态调整
-
发布与分发
- 用
cargo build --release
可生成优化过的可执行文件,它通常体积更小、运行更快;适合给他人使用或部署到生产环境。
- 用
-
更完善的错误提示
- 如果想知道具体的错误类型(如空输入、非数字、超出范围等),可以在
Err(e)
分支里分析e
,输出更个性化的错误信息。
- 如果想知道具体的错误类型(如空输入、非数字、超出范围等),可以在
十一、常见问题 (FAQ)
Q1: parse()
可以转什么类型?
任何实现了 FromStr
trait 的类型都行,比如 u8
, i32
, f64
等。我们只需在 let guess: u32
里显式指定想要的类型。
Q2: expect
、match
、?
的使用场景?
expect(...)
常用于简化处理,一旦失败就退出程序。match
适合需要自定义分支逻辑或在失败时继续其他操作。?
操作符适合在函数里把错误向上抛出,但对于交互式游戏来说,我们往往需要在错误处做更多处理,如提示信息和继续输入,因此match
会更灵活。
Q3: Rust 编译器为什么很“严苛”?
Rust 的编译器在编译期执行严格的所有权和借用检查,防止常见的内存错误(如空悬指针、数据竞争)。尽管上手时感觉被“逼迫”写额外的语法或注释,但长远看,这能极大降低运行期错误和调试成本。
十二、总结
通过这个“猜数字”小游戏,我们掌握了以下 Rust 开发基础要点:
- Cargo 工具链:
cargo new
初始化项目,cargo run
编译并执行,cargo build --release
发布优化版本。 - 依赖管理:在
Cargo.toml
中添加[dependencies]
后,Cargo 会自动下载并构建第三方库(crate)。 - I/O 与字符串处理:
read_line
读用户输入,使用trim()
去掉空白,再用parse()
转成数值。 - 错误处理与 Result:在 Rust 中显式处理错误,使用
expect
快捷退出或用match
做自定义逻辑。 - 循环与分支:
loop
和break
控制流程,match
匹配枚举值全面覆盖大小比较结果。 - 随机数与 trait:
rand::Rng
trait 定义随机数方法,通过.gen_range(1..=100)
生成区间随机整数。
Rust 的编译器在安全与性能之间找到了独特的平衡,令你能够用“近乎 C++ 的效率”写出“有如 Java 的安全性”的代码。虽然初学可能觉得严格的类型系统和所有权规则有点“繁琐”,但一旦掌握之后,你会发现它们能帮你在编译期就捕获大量潜在错误,让程序更健壮、更可维护。
欢迎你在此基础上继续挖掘更高级的 Rust 特性:如所有权与借用的更深层机制、生命周期与智能指针、并发编程(std::thread
、std::sync
)、宏系统等,或结合 Web、网络、嵌入式等领域的框架和库来发挥 Rust 的威力。
下一步建议
- 阅读官方文档 The Rust Programming Language 后续章节,深入掌握语言核心要素。
- 尝试更多有趣的小项目或到 Crates.io 搜索你所需的功能库,将它们整合起来感受 Rust 生态的魅力。
- 学习并编写测试(
cargo test
),编写注释并通过cargo doc --open
生成文档。
这就是本篇博客的全部内容。希望通过“猜数字”小游戏的开发,你能对 Rust 的开发流程和特性有一个体系化的认识。有什么疑问或想分享的想法,欢迎在评论区留言,让我们一起在实践中不断成长,成为更好的 Rustacean!
以上内容仅用于技术分享,所示示例并非唯一做法,读者可自由发挥、举一反三。在不断实践中体会 Rust 的优势和乐趣,愿我们都能写出更加可靠和高效的程序。