第 2 章:写小游戏(Programming a Little Game)
章节概要
本章通过实现一个猜数游戏,介绍变量声明、标准输入输出、随机数生成、循环控制与 match 匹配等基础语法。
本章图解

目录
- [2.1 建⽴新项⽬(Setting Up a New Project)](#2.1 建⽴新项⽬(Setting Up a New Project) "#%E5%BB%BA%E2%BD%B4%E6%96%B0%E9%A1%B9%E2%BD%ACsetting-up-a-new-project")
- [2.2 处理⼀次猜测输⼊(Processing a Guess)](#2.2 处理⼀次猜测输⼊(Processing a Guess) "#%E5%A4%84%E7%90%86%E2%BC%80%E6%AC%A1%E7%8C%9C%E6%B5%8B%E8%BE%93%E2%BC%8Aprocessing-a-guess")
- [2.3 ⽣成秘密数字(Generating a Secret Number)](#2.3 ⽣成秘密数字(Generating a Secret Number) "#%E2%BD%A3%E6%88%90%E7%A7%98%E5%AF%86%E6%95%B0%E5%AD%97generating-a-secret-number")
- [2.4 将猜测值与秘密数字进⾏⽐较(Comparing the Guess to the Secret](#2.4 将猜测值与秘密数字进⾏⽐较(Comparing the Guess to the Secret "#%E5%B0%86%E7%8C%9C%E6%B5%8B%E5%80%BC%E4%B8%8E%E7%A7%98%E5%AF%86%E6%95%B0%E5%AD%97%E8%BF%9B%E2%BE%8F%E2%BD%90%E8%BE%83comparing-the-guess-to-the-secret")
- [2.5 ⽀持多次猜测与循环(Allowing Multiple Guesses with Looping)](#2.5 ⽀持多次猜测与循环(Allowing Multiple Guesses with Looping) "#%E2%BD%80%E6%8C%81%E5%A4%9A%E6%AC%A1%E7%8C%9C%E6%B5%8B%E4%B8%8E%E5%BE%AA%E7%8E%AFallowing-multiple-guesses-with-looping")
- [2.6 最终完整代码](#2.6 最终完整代码 "#%E6%9C%80%E7%BB%88%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81")
正文内容
Game) 让我们通过动⼿完成⼀个实战项⽬,真正跳进 Rust 的世界!本章将通过在⼀个实际程序中运⽤常⻅的 Rust 概念来 向你介绍它们。你将接触到 let、match、⽅法(methods)、关联函数(associated functions)、外部 crate 等 概念!在后续章节中我们会深⼊探讨这些细节,⽽在本章中,你只需要先动⼿实践这些基础知识。 我们将实现⼀个经典的初学者编程练习:猜数游戏。游戏规则如下:程序⾸先⽣成⼀个 1 到 100 之间的随机整数。 随后提示玩家输⼊⼀个猜测值。玩家输⼊后,程序会指出该猜测值是太⼩了(Too small!)还是太⼤了(Too big!)。如果猜对了,游戏将输出⼀条祝贺消息并退出。
建⽴新项⽬(Setting Up a New Project)
⾸先,进⼊你在第 1 章创建的 projects ⽬录,使⽤ Cargo 创建⼀个新项⽬:
rust
cargo new guessing_game BASH
cd guessing_game
查看⽣成的 Cargo.toml ⽂件: package TOML name = "guessing_game" version = "0.1.0" edition = "2024" dependencies 正如在第 1 章看到的,cargo new 默认在 src/main.rs 中为我们⽣成了 Hello, world! 程序。你可以直接通过 cargo run 编译并运⾏它进⾏测试:
rust
cargo run BASH
Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished dev profile unoptimized + debuginfo target(s) in 1.50s Running target/debug/guessing_game Hello, world! cargo run 命令在需要快速迭代测试每个功能增量时⾮常有⽤。重新打开 src/main.rs,接下来我们所有的代码都 将写在这个⽂件中。
处理⼀次猜测输⼊(Processing a Guess)
猜数游戏的第⼀步是获取⽤户的输⼊,处理该输⼊,并检查其格式是否符合预期。⾸先,我们允许玩家输⼊猜测。 将代码清单 2-1 输⼊到 src/main.rs 中。
rust
// Listing 2-1: Code that gets a guess from the user and prints it RUST
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin() .read_line(&mut guess)
rust
.expect("Failed to read line");
println!("You guessed: {guess}");
}
这段简短的代码包含了⼤量的信息,让我们逐⾏深⼊剖析。 引⼊输⼊/输出库 为了获取⽤户输⼊并打印输出,我们需要将 io(输⼊/输出)库引⼊作⽤域。io 库来⾃标准库(即 std):
rust
use std::io; RUST
默认情况下,Rust 会在每个程序的开头⾃动将标准库中定义的⼀组最常⽤项引⼊作⽤域,这个集合称为 prelude (预导⼊模块)。如果某个你想使⽤的类型不在 prelude 中,就必须显式通过 use 语句将其引⼊当前作⽤域。
rust
fn main() { 依然是程序的执⾏⼊⼝,⽽随后的两⾏ println! 宏负责向屏幕打印游戏的提示语。
使⽤变量存储值(Storing Values with Variables) 接下来,程序创建了⼀个⽤于保存⽤户输⼊的变量:
rust
let mut guess = String::new(); RUST
这⼀⾏⾮常关键,涉及三个 Rust 基础特性:
let语句声明变量:Rust 使⽤ let 关键字创建变量。例如 let apples = 5;。- 变量默认是不可变的(immutable by default):在 Rust 中,⼀旦将⼀个值绑定到变量名上,该值就不能 再被改变。如果要让变量可变,必须在变量名前显式加上 mut 关键字:
rust
let apples = 5; // 不可变 RUST
let mut bananas = 5; // 可变
因此,let mut guess 声明了⼀个名为 guess 的可变变量。 3. String::new():等号右侧调⽤了 String::new() 函数,该函数返回⼀个全新的 String 实例。String 是标 准库提供的可增⻓、UTF-8 编码的堆上字符串类型。
-
语法中的 :: 表明 new 是 String 类型的关联函数(associated function)(在某些语⾔中也称静态⽅ 法)。关联函数是直接实现于类型本身之上的函数。 总结这⼀⾏:我们创建了⼀个名为 guess 的可变变量,它当前绑定到了⼀个全新的空 String 实例上。 接收⽤户输⼊(Receiving User Input) 接着,我们调⽤ io 模块的 stdin 函数来处理标准输⼊: io::stdin() RUST .read_line(&mut guess)
-
io::stdin() 返回 std::io::Stdin 的⼀个实例,代表终端的标准输⼊句柄。
-
随后在该句柄上调⽤ .read_line(&mut guess) ⽅法,从标准输⼊中读取⼀⾏内容,并将其追加到传⼊的字 符串中。
-
引⽤与借⽤(&):参数为 &mut guess。& 表示该参数是⼀个引⽤(reference)。引⽤允许代码的不同部分 访问同⼀块内存数据,⽽⽆需在内存中多次复制该数据。
-
与变量⼀样,Rust 中的引⽤默认也是不可变的。因此,为了让 read_line 能够向字符串中写⼊数据,必须 显式写作 &mut guess(可变引⽤),⽽不是 &guess。 使⽤ Result 类型处理潜在的失败(Handling Potential Failure with the Result Type) 紧接在 read_line 后⾯的代码是: .expect("Failed to read line"); RUST 虽然它被分⾏书写以提⾼可读性,但在逻辑上与前⾯是同⼀条表达式:
-
read_line 除了把输⼊存⼊字符串外,还会返回⼀个
Result枚举值。 -
Result 是⼀个枚举(enumeration / enum),⽤于编码错误处理信息,包含两个变体(variants):
Ok与Err。 -
Ok 变体表示操作成功,内部封装了成功产⽣的值(在 read_line 的情况下是读取的字节数)。
-
Err 变体表示操作失败,内部封装了错误的相关信息。
-
Result 类型上定义了 .expect() ⽅法:
-
如果 Result 是 Err,expect 会使程序崩溃(panic)并打印传⼊的错误提示字符串。
-
如果 Result 是 Ok,expect 会提取出 Ok 内部包含的值并返回给你。
-
如果不调⽤ expect,代码虽然能编译,但编译器会发出警告,提示未处理可能出错的 Result。在当前场景 下,如果读取失败直接崩溃是合理的处理⽅式,因此我们使⽤ expect。 使⽤占位符输出值(Printing Values with println! Placeholders) 第⼀阶段的最后⼀⾏代码: println!("You guessed: {guess}"); RUST 花括号 {} 充当格式化占位符(书中⻛趣地⽐喻为"蟹钳夹住⼀个值")。如果打印的是变量,变量名可以直接写在 {} 内部;如果要打印表达式的求值结果,可以使⽤空括号 {} 并在格式化字符串后⽤逗号按顺序传⼊表达式:
rust
let x = 5; RUST
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2); // 输出:x = 5 and y + 2 = 12 测试第⼀部分 运⾏ cargo run 测试程序:
rust
cargo run BASH
Finished dev profile unoptimized + debuginfo target(s) in 6.44s Running target/debug/guessing_game Guess the number! Please input your guess. 6 You guessed: 6 到⽬前为⽌,我们成功从键盘读取了输⼊并打印了出来!
⽣成秘密数字(Generating a Secret Number)
接下来,我们需要⽣成⼀个 1 到 100 之间的随机秘密数字。Rust 的核⼼标准库尚未直接包含随机数功能,但 Rust 团队在 crates.io 上提供了名为 rand 的 crate。 使⽤ Crate 增加功能(Increasing Functionality with a Crate) 在 Rust 中,代码包称为 crate:
- 我们正在构建的项⽬是⼀个⼆进制 crate(binary crate),它可以被编译为可执⾏⽂件。
- rand 则是⼀个库 crate(library crate),它包含供其他程序引⽤的库代码,不能独⽴作为程序运⾏。
要使⽤ rand,我们需要编辑 Cargo.toml,在 dependencies 段落下⽅添加依赖项: dependencies TOML rand = "0.8.5" 这⾥的 "0.8.5" 遵循语义化版本规范(Semantic Versioning / SemVer),它实际上代表 ^0.8.5,即兼容该版本 的最新补丁包(>= 0.8.5 且 < 0.9.0)。 保存后直接运⾏ cargo build:
- Cargo 会更新 crates.io 索引,并⾃动分析下载 rand 及其所依赖的所有底层 crates(例如 libc、rand_core 等)。
- 随后编译所有依赖项,并最终编译你的项⽬。 可重现构建:Cargo.lock 与依赖升级
- Cargo.lock:⾸次运⾏ cargo build 时⽣成的锁⽂件,精确记录了实际拉取的每个依赖包的准确版本。此后 再次构建时,Cargo 会严格按照 Cargo.lock 中记录的版本进⾏构建,确保任何⼈在任何时间编译该项⽬都能获 得完全⼀致的结果。
- cargo update:当你希望更新依赖包时,运⾏ cargo update 会忽略当前的锁⽂件,计算符合 Cargo.toml 要求的最新版本,并写回 Cargo.lock(例如在 0.8.x 范围内更新⾄ 0.8.6)。 编写⽣成随机数的代码 修改 src/main.rs,引⼊ rand 并⽣成随机数(代码清单 2-3):
rust
// Listing 2-3: Adding code to generate a random number RUST
use std::io;
use rand::Rng;
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.");
let mut guess = String::new();
io::stdin() .read_line(&mut guess)
rust
.expect("Failed to read line");
println!("You guessed: {guess}");
}
use rand::Rng;:引⼊了 Rng trait(特征)。Trait 定义了随机数⽣成器实现的⽅法;要使⽤这些⽅法,该
rust
trait 必须显式在作⽤域内(在第 10 章会详细讲解 trait)。
rand::thread_rng():获取当前执⾏线程专属、并由操作系统播种的随机数⽣成器。.gen_range(1..=100):调⽤ Rng trait 上的 gen_range ⽅法,接收⼀个范围表达式。1..=100 表示⼀个包 含上限与下限的闭区间(即 1 到 100 之间)。- 我们临时加⼊了⼀⾏输出秘密数字的语句,⽅便在开发调试过程中验证逻辑。 提示:想要了解某个第三⽅库包含哪些⽅法?在终端运⾏ cargo doc --open,Cargo 会在本地构建所有依 赖项的 API ⽂档并在浏览器中打开供你查阅!
将猜测值与秘密数字进⾏⽐较(Comparing the Guess to the Secret
Number) 现在我们有了⽤户输⼊的字符串和⽣成的秘密数字,需要对它们进⾏⽐较(代码清单 2-4)。
rust
use std::cmp::Ordering; RUST
use std::io;
use rand::Rng;
fn main() {
// --snip--
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"),
rust
}
}
- 从标准库中引⼊ std::cmp::Ordering 枚举。它的三个变体分别是 Less、Greater 和 Equal。
cmp⽅法:可以在任何可⽐较的值上调⽤,它接收⽬标值的引⽤(&secret_number),并返回⼀个 Ordering 枚举变体。match表达式:Rust 中强⼤的控制流结构。它由多个分⽀(arms)组成,每个分⽀包含⼀个匹配模式 (pattern)和模式匹配成功后要执⾏的代码。Rust 会从上到下按顺序匹配分⽀模式,并执⾏匹配成功的第⼀个 分⽀。 类型不匹配错误与类型转换(Type Inference & Shadowing) 如果你现在尝试编译上⾯的代码,会遇到编译报错: errorE0308: mismatched types PLAINTEXT --> src/main.rs:22:21 |
rust
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected &String, found &{integer} 错误原因在于:
- Rust 是⼀⻔静态强类型语⾔,guess 是 String 类型,⽽ secret_number 是数字类型(默认为 32 位有符号 整数 i32),Rust 不允许直接将字符串与数字进⾏⽐较!
- 我们需要将⽤户输⼊的 String 解析转换为数字类型。 在 read_line 后追加以下代码:
rust
let guess: u32 = guess.trim().parse().expect("Please type a number!"); RUST
这⾥包含了⼏个核⼼要点:
- 变量隐藏(Shadowing):我们声明了⼀个同样名为 guess 的新变量。Rust 允许我们⽤新声明的变量遮蔽 (shadow)原有的同名变量。这在做类型转换时⾮常⽅便,省去了不得不取诸如 guess_str 与 guess_num 之 类的命名困扰。
trim():去掉字符串前后的空⽩字符。⽤户在终端输⼊并按下回⻋时,输⼊字符串末尾会带有换⾏符 (Linux/macOS 为 \n,Windows 为 \r\n),trim() 会剔除它们,只留下纯数字字符。parse():将字符串解析为⽬标类型。我们在变量声明时显式标注了类型 : u32(32 位⽆符号整数),这指 导了 parse() 知道该将其转换为什么数字类型。(同时,Rust 也会依据该类型推断 secret_number 也应是 u32 类型)。- 再次使⽤
expect():字符串解析可能会失败(例如输⼊包含了字⺟或符号),因此 parse() 返回 Result。如果遇到错误,expect() 会直接让程序 panic 并打印错误提示。 此时再次编译并运⾏代码,即可成功⽐较⼤⼩!
⽀持多次猜测与循环(Allowing Multiple Guesses with Looping)
⽬前程序在玩家猜了⼀次之后就会退出。为了让游戏能持续玩下去,我们使⽤ loop 关键字创建⼀个⽆限循环:
rust
// --snip-- RUST
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin() .read_line(&mut guess)
rust
.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!"),
rust
}
}
现在程序会不断循环要求⽤户输⼊。但此时出现了⼀个新问题:程序永远停不下来,除⾮⽤户通过 Ctrl-C 强制中 断,或者输⼊⼀个⾮数字内容让 parse().expect() 发⽣ panic 崩溃退出。 猜对后退出循环(break) 当⽤户猜中数字(Ordering::Equal)时,我们应该让游戏正常退出。我们可以在匹配分⽀中加上 break 语句:
rust
match guess.cmp(&secret_number) { RUST
Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"),
rust
Ordering::Equal => {
println!("You win!");
break;
}
}
当 break 被执⾏时,程序将⽴即跳出 loop 循环;由于循环是 main 函数的最后⼀部分,跳出后程序也将随之正常 退出。 优雅处理⽆效输⼊(Handling Invalid Input) 现在如果⽤户输⼊了⾮数字字符(例如⼿滑输错了字⺟),程序会直接崩溃(panic)。更好的做法是忽略⾮数字输 ⼊,并提示⽤户继续输⼊下⼀次猜测。 我们不再调⽤ expect,⽽是改⽤ match 表达式来精细化处理 parse() 返回的 Result(代码清单 2-5):
rust
// Listing 2-5: Ignoring a non-number guess and asking for another guess RUST
let guess: u32 = match guess.trim().parse() {
Ok(num) => num, Err(_) => continue,
rust
};
- 如果 parse() 解析成功,返回 Ok(num),匹配第⼀个分⽀,直接将内部的数字 num 作为整个 match 的求值 结果赋值给新的 guess 变量。
- 如果解析失败,返回 Err(_)。这⾥的下划线 _ 是⼀个通配符模式,表示匹配任何错误信息⽽不绑定其具体内 容。匹配到该分⽀时执⾏
continue,这会指示程序直接跳过当前循环的剩余代码,进⼊下⼀次迭代,等待⽤户 的下⼀次输⼊!
最终完整代码
在完成上述改造后,我们删除掉输出秘密数字的测试代码(否则⼀开局就剧透了答案)。最终的完整程序如代码清 单 2-6 所示:
rust
// Listing 2-6: The complete guessing game code RUST
use std::cmp::Ordering;
use std::io;
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)
rust
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num, Err(_) => continue,
rust
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"),
rust
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
现在运⾏测试:
rust
cargo run BASH
Guess the number! Please input your guess. 50 You guessed: 50 Too small! Please input your guess.
75 You guessed: 75 Too big! Please input your guess. foo Please input your guess. 63 You guessed: 63 You win! 可以看到,输⼊ foo 等⾮数字时,程序不会崩溃,⽽是优雅地忽略错误并等待再次输⼊;猜中数字时输出 You win! 并退出游戏。 本章⼩结(Summary) 通过这个猜数游戏实战,你已经亲身体验并实践了许多核⼼的 Rust 编程概念:
- 变量与可变性:使⽤ let 声明变量,变量默认不可变,使⽤ mut 显式声明可变性。
- 引⽤与借⽤:使⽤ & 与 &mut 传递内存引⽤。
- 关联函数:通过 String::new() 等语法在类型级别调⽤函数。
- 外部 Crate:在 Cargo.toml 中声明依赖、理解 SemVer 规范,并通过 use rand::Rng; trait 扩展功能。
- 枚举与模式匹配:理解包含变体的 Result 和 Ordering 枚举,并⽤强⼤的 match 表达式处理分⽀。
- 变量遮蔽与错误处理:通过 shadowing 优雅转换数据类型,并利⽤ match 对 Result 的 Ok 与 Err 进⾏安 全恢复处理。
- 控制流:通过 loop、break 和 continue 控制程序的执⾏周期。 在接下来的第 3 章中,我们将系统性地梳理通⽤编程概念(变量、标量与复合数据类型、函数、语句与表达式、控 制流等)在 Rust 中的语法规范与底层机制。 声明变量,变量默认不可变,使⽤ mut 显式声明可变性。
- 引⽤与借⽤:使⽤ & 与 &mut 传递内存引⽤。
- 关联函数:通过 String::new() 等语法在类型级别调⽤函数。
- 外部 Crate:在 Cargo.toml 中声明依赖、理解 SemVer 规范,并通过 use rand::Rng; trait 扩展功能。
- 枚举与模式匹配:理解包含变体的 Result 和 Ordering 枚举,并⽤强⼤的 match 表达式处理分⽀。
- 变量遮蔽与错误处理:通过 shadowing 优雅转换数据类型,并利⽤ match 对 Result 的 Ok 与 Err 进⾏安 全恢复处理。
- 控制流:通过 loop、break 和 continue 控制程序的执⾏周期。 在接下来的第 3 章中,我们将系统性地梳理通⽤编程概念(变量、标量与复合数据类型、函数、语句与表达式、控 制流等)在 Rust 中的语法规范与底层机制。