Bootstrap

Rust 猜数字游戏:从 0 到 1 的完整实现与深入解析

一、项目概述

1.1 为什么选择“猜数字”?

“猜数字”是编程入门中非常经典的一个项目。它看似简单,却能很好地展示:

  1. 输入输出 (I/O):提示用户输入并读取内容。
  2. 随机数:每次运行生成一个随机值,保证游戏的多样性。
  3. 逻辑判断与控制流:比较大小并给出提示。
  4. 错误处理:当用户输入不合法内容时,我们如何应对?
  5. 循环:允许玩家多次猜测,直到猜对为止。

这些功能组合起来,就能覆盖 Rust 中相当多的常用特性和写法,非常适合用来练手和深入理解语言机制。

1.2 本文内容预览

  1. 环境准备与项目初始化:安装 Rust,创建新项目结构。
  2. 用户输入与输出:使用 Rust 标准库 std::io 读写命令行数据。
  3. 随机数生成:学习如何在 Cargo.toml 中添加依赖并使用社区库 rand
  4. 字符串解析与比较:将输入的字符串转为数字,并比较与随机数的大小。
  5. 循环与错误处理:通过 loop 允许多次猜测,使用 match 处理各种错误场景。
  6. 总结与扩展:提供改进思路,例如多模式输入、添加回合数等。

在这个过程中,我们将看到 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}");
}

逐行分析:

  1. println!("Guess the number!"):打印游戏开场信息。
  2. println!("Please input your guess."):提示玩家输入。
  3. let mut guess = String::new();:创建一个可变字符串 guess 用于存储用户输入。
  4. io::stdin().read_line(&mut guess):从标准输入读取一行文本并附加到 guess
  5. .expect("Failed to read line"):如果读取失败,就打印错误并退出程序。
  6. 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::Orderingmatch 进行比较

Rust 的比较函数 cmp 会返回一个 Ordering 枚举,可能是 LessGreaterEqual。我们用 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 关键机制与编译特性

  1. 所有权与借用 (Ownership & Borrowing)

    • read_line(&mut guess) 里,我们传的是 &mut guess(一个可变引用),保证同时只有一个对该数据的可变引用,从而避免了并发读写时的竞争。
    • Rust 编译器会在编译期自动帮你检查借用规则,如果出现违规借用,代码就无法通过编译。
  2. Result 枚举与错误处理

    • read_lineparse 这样的函数都返回一个 Result。它可能是 Ok(T) 表示成功,也可能是 Err(E) 表示失败,并携带错误信息。
    • Rust 强制在编译期显式处理错误,这能让代码更健壮,减少运行期意外崩溃。
  3. match 表达式

    • match 要求你列出所有可能的匹配分支,编译器会检查是否有遗漏;这也符合 Rust“让潜在错误在编译期就暴露”的设计理念。
    • 例如,我们用 match guess.cmp(&secret_number) 彻底覆盖了 LessGreaterEqual 三个场景。
  4. Cargo 编译缓存

    • 在第一次编译或添加新依赖时,Cargo 可能要下载并编译更多内容。但只要你后续没变动这些依赖,Cargo 不会重复下载编译,执行 cargo buildcargo run 都会非常高效。
  5. 可移植性与跨平台

    • 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!” 提示可以帮助玩家逐步逼近答案,直到猜中后结束。

这就是我们想要的完整小游戏体验!

十、可能的扩展与改进

  1. 自定义退出命令

    • 在玩家想要提前退出时,可以输入 “q” 或 “quit”。实现上,可以在 parse() 前先判断 guess.trim() 是否等于 "q" 等,再决定是退出还是继续。
  2. 增加回合数统计

    • 可以在循环外声明一个 let mut attempts = 0;,每猜一次就 attempts += 1;。猜中后告诉玩家“你总共猜了 x 次”。
  3. 多重难度

    • 在游戏开始时让用户选择难度,比如简单难度(150)和困难难度(11000)。根据选择,动态调整 gen_range 的范围。
  4. 发布与分发

    • cargo build --release 可生成优化过的可执行文件,它通常体积更小、运行更快;适合给他人使用或部署到生产环境。
  5. 更完善的错误提示

    • 如果想知道具体的错误类型(如空输入、非数字、超出范围等),可以在 Err(e) 分支里分析 e,输出更个性化的错误信息。

十一、常见问题 (FAQ)

Q1: parse() 可以转什么类型?
任何实现了 FromStr trait 的类型都行,比如 u8, i32, f64 等。我们只需在 let guess: u32 里显式指定想要的类型。

Q2: expectmatch? 的使用场景?

  • expect(...) 常用于简化处理,一旦失败就退出程序。
  • match 适合需要自定义分支逻辑或在失败时继续其他操作。
  • ? 操作符适合在函数里把错误向上抛出,但对于交互式游戏来说,我们往往需要在错误处做更多处理,如提示信息和继续输入,因此 match 会更灵活。

Q3: Rust 编译器为什么很“严苛”?
Rust 的编译器在编译期执行严格的所有权和借用检查,防止常见的内存错误(如空悬指针、数据竞争)。尽管上手时感觉被“逼迫”写额外的语法或注释,但长远看,这能极大降低运行期错误和调试成本。

十二、总结

通过这个“猜数字”小游戏,我们掌握了以下 Rust 开发基础要点:

  1. Cargo 工具链cargo new 初始化项目,cargo run 编译并执行,cargo build --release 发布优化版本。
  2. 依赖管理:在 Cargo.toml 中添加 [dependencies] 后,Cargo 会自动下载并构建第三方库(crate)。
  3. I/O 与字符串处理read_line 读用户输入,使用 trim() 去掉空白,再用 parse() 转成数值。
  4. 错误处理与 Result:在 Rust 中显式处理错误,使用 expect 快捷退出或用 match 做自定义逻辑。
  5. 循环与分支loopbreak 控制流程,match 匹配枚举值全面覆盖大小比较结果。
  6. 随机数与 traitrand::Rng trait 定义随机数方法,通过 .gen_range(1..=100) 生成区间随机整数。

Rust 的编译器在安全与性能之间找到了独特的平衡,令你能够用“近乎 C++ 的效率”写出“有如 Java 的安全性”的代码。虽然初学可能觉得严格的类型系统和所有权规则有点“繁琐”,但一旦掌握之后,你会发现它们能帮你在编译期就捕获大量潜在错误,让程序更健壮、更可维护。

欢迎你在此基础上继续挖掘更高级的 Rust 特性:如所有权与借用的更深层机制、生命周期与智能指针、并发编程(std::threadstd::sync)、宏系统等,或结合 Web、网络、嵌入式等领域的框架和库来发挥 Rust 的威力。

下一步建议

  • 阅读官方文档 The Rust Programming Language 后续章节,深入掌握语言核心要素。
  • 尝试更多有趣的小项目或到 Crates.io 搜索你所需的功能库,将它们整合起来感受 Rust 生态的魅力。
  • 学习并编写测试(cargo test),编写注释并通过 cargo doc --open 生成文档。

这就是本篇博客的全部内容。希望通过“猜数字”小游戏的开发,你能对 Rust 的开发流程和特性有一个体系化的认识。有什么疑问或想分享的想法,欢迎在评论区留言,让我们一起在实践中不断成长,成为更好的 Rustacean!

以上内容仅用于技术分享,所示示例并非唯一做法,读者可自由发挥、举一反三。在不断实践中体会 Rust 的优势和乐趣,愿我们都能写出更加可靠和高效的程序。

;