Skip to main content

C2 一个简单的示例项目 Guessing Game

https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html

这个示例项目用于介绍 Rust 的一些基本概念, 包括 let, match, 关联函数, 外部 crate 等.

这个示例项目的需求为: app 内部生成一个 1 到 100 的随机数, 然后提示用户输入一个数, 如果输入的数和生成的一致, 则玩家赢. 同时玩家可以多次猜数, 如果输入的数大了, 则提示大了, 如果输入的数小了, 则提示小了.

开发过程

获取用户输入

  1. 使用 cargo new guessing_game 创建一个新工程.

  2. 首先需要让用户输入一个数, 在 Rust 中获取标准输入可以通过如下方式:

    io::stdin().read_line(&mut buf).unwrap();

    要使用上述代码, 首先需要引入标准库中的 iouse std::io;. Rust 提供有 prelude 机制, 可以自动隐式引入一些常用的库, 如果需要的库不在 prelude 范围, 则手动显式引入即可.

  3. 将输入的数放入到 buf 中, 此时可以使用如下:

    let mut guess = String::new();

    其中 new 为 String 类型的关联函数(可以理解为类方法).

    此时可以将标准输入的值放入到 guess 中:

    io::stdin().read_line(&mut guess);

    其中 io::stdin() 用于获取一个标准输入句柄对象, 在其上调用 read_line 对象方法, 该方法需要一个缓冲区参数, 传入我们创建的 String 即可.

    在传入时使用的是 & 表示传入缓冲区的引用, 引用的作用是可以让代码中多处位置使用相同的数据, 而无需将数据复制多份. 而 &mut 的作用是表示数据是可变的引用.

  4. read_line 的结果是一个 Result, 使用 unwrapexpect 可以在其值为 Ok 的时候返回该值, 而 Err 时触发 Panic.

生成随机数

为生成随机数, 可以使用 rand crate, 在 Cargo.toml 中添加如下依赖:

[dependencies]
rand = "0.8.5"

添加依赖后, 可以发现生成了一个 Cargo.lock 文件, 它是 Cargo 用来保证每次 Build 均能生成一致产物的机制, 其中记录了添加时指定的依赖版本. 除非用户手动执行 cargo update 更新依赖版本(在更新依赖版本时, 会按指定的 minor 更新, 而不会更新到大版本).

下面使用 rand 生成随机数:

use rand::Rng;

// ...

let number = rand::thread_rng().gen_range(1..=100);

当不确定该使用依赖中什么类型/什么方法时, 可以执行 cargo doc --open 命令生成依赖列表中所有依赖的文档并在本地打开, 这样就可以通过文档来看到了.

对比生成随机数和用户输入

对比时, 可以调用 guesscmp 方法, 其结果是一个 enum, 包含 Ordering 下的 Less, Greater, Equal 三种情况. Rust 提供了一个 match 可以进行模式匹配:

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}

但此时对比是无法进行的, 因为 guesssecret_number 的类型不一致(一个是 String, 一个是 u32). 我们可以通过如下方式将 String 转换为 u32:

let guess: u32 = guess.trim().parse().expect("Please type a number");
  1. 这里再定义了一个和用户输入 String 同名的 guess 变量, Rust 有变量名覆盖机制, 这样可以让名字在下一次仍然可用.
  2. trim 用于将字符串中的空白符移除
  3. parse 用于将字符串转换为对应类型的值, 若转换失败, 产生 Result 的 Err.

完成阶段性目标

到目前为止, 已经完成了 生成随机数 -> 用户输入并解析为数字 -> 对比 三个步骤, 下面是汇总代码:

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!"),
}

我们还需要实现一个需求: 用户能反复猜数字.

反复猜数字

Rust 提供了一个实现无限循环的办法: loop 循环可以达到这个目的:


// 生成随机数并解析...

loop {
// 用户输入...
// 对比并输出结果...
}

通过上述结构结合第一阶段代码, 即可实现完整功能.

还有, 如果想让用户猜对后退出循环并结束, 也非常简单, 使用 break 中断循环即可:

// ...
loop {
// ...
match guess.cmp(&secret_number) {
//...
Ordering::Equal => {
println!("You win!");
break; // 可以中断外层 loop
}
}
}

用户无效输入的处理

针对用户的输入 parse, 我们之前是直接终止程序运行的, 需要优化体验, 而办法也非常简单, 只需要处理 parse 时候的 Result 即可:

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

完整代码

完整代码如下:

use std::io;
use rand::Rng;

fn main() {
let secret_number: u32 = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess");
let mut guess = String::new();
io::stdin().read_line(&mut guess).unwrap();
let guess: u32 = match guess.trim().parse() {
Ok(n) => n,
Err(_) => {
continue;
}
};
println!("Your guess is: {guess}");
match guess.cmp(&secret_number) {
std::cmp::Ordering::Less => {
println!("Too small");
continue;
}
std::cmp::Ordering::Greater => {
println!("Too big");
continue;
}
std::cmp::Ordering::Equal => {
println!("You win!");
break;
}
}
}
}

C2 完结, C3 通用编程概念 待续.