Cheyne's Blog


  • Home
  • Archive
  • Categories
  •    

© 2025 John Doe

Theme Typography by Makito

Proudly published with Hexo

The Rust Programming Language:Programming a Guessing Game

Posted at 2025-08-29 Rust 

在本章中,我们将会从零开始实现一个经典的入门级程序——猜数字小游戏,目的是在编写的过程中快速熟悉Rust中的各个概念。这个小游戏的主要规则为:随机生成一个1到100的数字,然后提示玩家进行猜测,当玩家输入一个数字后,需要提醒玩家做出的猜测太大了或者是太小了,直到正确地猜到目标数。

Setting Up a New Project

首先,让我们新建一个工程:

1
2
$ cargo new guessing_game
$ cd guessing_game

此时就会生成如第一章中所提到的目录结构,我们将在src/main.rs这个文件中编写代码。

Processing a Guess

游戏的第一个部分是需要获取玩家的输入,处理这个输入,并且检查这个输入是否是我们期望的格式(整型数)。那么首先,我们需要让玩家输入一个猜测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
.expect("Failed to read line");

println!("You guessed: {}", guess);
}

这段代码包含了很多内容,让我们来一行行地分析它。
为了获取用户的输入,我们需要引入io库。这个io库来自于称为std的标准库:

1
use std::io;

Rust在标准库中定义了很多内容,这些内容会默认被引入到程序中,称为预导入(prelude)。如果你需要使用的内容没有被包含在prelude里面,就需要显式地用use将其引入到程序中。std::io这个标准库提供了很多有用的功能,包含接收用户输入的能力。

1
fn main() {

main函数是程序的入口。fn用于声明一个新的函数。

Storing Values with Variables

接下来,我们创建一个变量来储存用户的输入:

1
let mut guess = String::new();

这简单的一行里面也包含了很多内容。我们可以使用let来创建变量,例如:

1
let apples = 5;

这里创建了一个新的变量apples并将其赋值为5。在Rust中,**变量在默认情况下是不可变的(常量),如果要让其可变,则需要在变量名前添加mut**:

1
2
let apples = 5; // immutable
let mut bananas = 5; // mutable

回到猜数字程序中,我们现在知道了let mut guess创建了一个可变的变量guess,并将其赋值为了String::new函数的调用结果,一个String实例。String是标准库提供的字符串类型,它是一个可增长的、UTF-8编码的文本字符串。
::new中的::语法表示new是String类型中的一个关联函数(associated function)。associated function是实现在类型上的函数。new函数创建了一个新的,空的字符串。

associated function <-> static function

Receiving User Input

现在我们已经引入了io库,我们将调用其中的stdin函数来获取用户输入:

1
2
io::stdin()
.read_line(&mut guess)

如果我们没有用use std::io来引入io库,我们也可以用std::io::stdio来调用stdin函数。这个函数返回了一个std::io::stdin实例,让我们可以处理用户在终端中的标准输入。
下一行,.read_line(&mut guess)调用了read_line方法以获取用户输入。这里传入的字符串参数需要是可变的,以便该方法更改字符串内容。**&这个符号表示传入的参数是一个引用,这能直接指向数据,避免了将数据多次地拷贝到内存当中。引用是很复杂的功能,Rust主要的优点之一就是它在使用引用时的安全性和简便性。目前你不需要知道太多关于引用的细节,只需要知道,跟变量一样,引用在默认情况下是不可变的**,因此你需要写&mut guess而不是&guess。

Handling Potential Failure with Result

我们继续分析上面的这行代码,它的下一部分是:

1
.expect("Failed to read line");

同上面所说的那样,read_line将用户的输入接收到了,但它返回的是一个Result,这是一个枚举值(enum),包含Ok和Err。显然,我们需要对Response进行错误处理。Ok表示操作成功,并且其中会包含成功生成的值,而Err则表示操作失败,其中会包含错误信息。
expect是定义在Response类型中的一个方法。如果Result是Err,expect将会导致程序崩溃并将参数作为错误信息进行展示。而如果Result是Ok,expect则会返回它所包含的值,在当前情况下,这个值应该是用户输入的字节数。
如果你没有调用expect,这个程序也能正常编译,但是你会接收到一个warning:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cargo build            
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | / io::stdin()
11 | | .read_line(&mut guess);
| |______________________________^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin()
| +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s

Rust警告你没有使用read_line返回的Result值,这表示你没有对可能产生的错误进行处理。
正确的警告处理方式是编写错误处理相关的代码,但是在当前的程序中我们想在错误发生时让程序crash掉,因此我们直接使用了expect。

Printing Values with println! Placeholders

目前还没有分析的就只剩下一行:

1
println!("You guessed: {}", guess);

这里的{}是一个占位符,会将guess的值传入到占位符中。
此外,也可以像这样直接用一次println!调用来打印一个变量和一个表达式的结果:

1
2
3
4
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

Testing the First Part

让我们测试一下程序的第一部分,运行cargo run:

1
2
3
4
5
6
7
8
$ cargo run
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1

Generating a Secret Number

接下来,我们需要生成一个数字以供用户进行猜测,这个数字在每次启动程序时都应该是不同的。Rust没有在标准库中包含随机数生成的能力,但是Rust团队提供了一个rand crate来支持它。

Using a Crate to Get More Functionality

一个crate是一个Rust源代码文件的集合。我们现在构建的项目是一个binary crate,它是可执行的。而rand crate是一个library crate,其中包含的代码旨在被其他程序使用,无法单独执行。
在我们在编写使用rand的代码之前,我们需要修改Cargo.toml文件来引入rand crate作为依赖:

1
2
[dependencies]
rand = "0.8.5"

这里的0.8.5是^0.8.5的缩写,表示版本至少是0.8.5但低于0.9.0。
现在,无需更改任何代码,让我们编译这个程序:

1
2
3
4
5
6
7
8
9
10
11
$ cargo build
Compiling cfg-if v1.0.0
Compiling libc v0.2.172
Compiling zerocopy v0.8.25
Compiling getrandom v0.2.16
Compiling rand_core v0.6.4
Compiling ppv-lite86 v0.2.21
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.02s

当我们包含一个外部依赖时,Cargo会从registry中获取到该依赖所需的所有最新的版本数据,这些数据是Crates.io的副本。Crates.io是Rust生态中人们发布他们的开源Rust项目,以供他人使用的地方。
当更新完registry之后,Rust会下载没有下载过的crates并下载每个crate所依赖的crates。
如果你没有更新[dependencies]中的crates,即使再次运行cargo build也不会再次触发下载依赖的动作,甚至修改代码后再cargo build也是同样的。

Ensuring Reproducible Builds with the Cargo.lock File

当第一次构建项目时,Cargo会自动找出符合Cargo.toml中条件的所有依赖项版本,然后将其写入Cargo.lock文件。当将来构建项目时,Cargo会看到Cargo.lock文件存在,并会使用其中指定的版本,而不是重新进行版本计算。这可以实现可重现的构建。

Updating a Crate to Get a New Version

当你想要升级一个crate时,Cargo提供了update命令,这将忽视Cargo.lock文件并找出符合Cargo.toml中条件的依赖的最新版本。在当前的情况下,Cargo将会寻找大于0.8.5并且小于0.9.0的rand版本。也就是说,假如现在rand存在0.8.6和0.9.0这两个版本,那么cargo update将会将rand升级到0.8.6。

Generating a Random Number

现在我们安装好了依赖,就可以使用rand来生成一个随机数了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
.expect("Failed to read line");

println!("You guessed: {guess}");
}

首先我们添加了use rand::Rng;。Rng特性(trait)定义了随机数生成器实现的方法。
接下来我们在中间添加了两行代码。第一行我们调用了rand::thread_rng()函数来获取我们将要使用的随机数生成器,这个生成器是在当前线程本地执行并由操作系统进行seed的。然后我们在这个生成器上调用了gen_range方法,这个方法是由Rng特性定义的,它接收了一个范围表达式作为参数并生成了一个范围内的随机数。我们使用的这种范围表达式的语法为start..=end,它是一个闭区间。
第二行则是将这个随机数进行打印。
现在我们尝试多次运行这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 47
Please input your guess.
47
You guessed: 47

$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 74
Please input your guess.
74
You guessed: 74

$ cargo run
Compiling guessing_game v0.1.0 (/Volumes/SN580/Users/cheyne/Documents/study/rust/the_rust_programming_language/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 87
Please input your guess.
87
You guessed: 87

可以看到每次运行都生成了一个不同的,在1到100这个范围内的随机数。

Comparing the Guess to the Secret Number

现在我们有了用户的输入和一个随机数,就可以对它们进行比较了。这一步的代码如下所示,但是它暂时无法成功编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use rand::Rng;
use std::cmp::Ordering;
use std::io;

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

首先我们用另一个use将std::cmp::Ordering这一类型从标准库中导入。这个Ordering类型也是一个枚举值,包含Less,Greater和Equal。
然后,cmp方法对两个值进行了比较,它可以在任何可以被比较的值上进行调用。它接收一个你想比较的值的引用,在这里将guess与secret_number进行比较。它将返回一个Ordering作为比较结果,我们使用了match表达式来对比较结果进行匹配,根据结果来决定下一步将执行哪一个分支的逻辑。

match <-> switch

一个match表达式由arms构成。一个arm由一个pattern和在给定的值符合该arm的pattern时应运行的代码所组成。Pattern和match的构造是Rust的强大特性:它们让你能处理代码可能遇到的各种情况,并确保你处理了所有的情况。

然而,这段代码并不能成功运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cargo build
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /path/to/your/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/cmp.rs:964:8
|
964 | fn cmp(&self, other: &Self) -> Ordering;
| ^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error

错误信息中显示存在不匹配的类型。Rust具有强大的静态类型系统,然而,它也具有类型推断的能力。当我们编写let mut guess = String::new()时,Rust能够推断出guess应该是一个String,并且不会要求我们显式编写出这个类型。而对于整型数字而言,除非另有说明,Rust默认使用i32类型,这里的secret_number就是这个类型,除非在其他地方添加类型信息使Rust推断出不同的数据类型。因此,这里的错误原因就是Rust不能对字符串和数字这两种类型进行比较。
所以,我们需要将用户输入从String类型转为数字类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// --snip--

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

这里我们又创建了一个不可变的变量guess,但是我们不是在之前已经定义过一个guess变量了吗?这就涉及到了Rust的另一个特性——Shadowing,它能允许我们用一个新的guess去覆盖旧的那个。在这里,我们可以直接复用guess这个变量名而不是创建出两个不同的变量guess_str和guess。这个特性在你想要将变量从一个类型转换到另一个类型的时候是很常用的。
trim方法比较常见,当用户输入5并按下回车键时,guess接收到的值将会是5\n(在Windows上为5\r\n),trim可以将首尾的空格和回车键等删除掉,只返回5。
字符串上的parse方法用于将其转换为另一种类型。在这里,我们将其转换为数字。通过let guess: u32,我们告诉了Rust我们想要的确切的数字类型为u32。
此外,这里的u32和与secret_number的比较也意味着Rust会将secret_number的类型也推断为u32。因此,现在的比较将是在两个相同类型的值之间进行的。
而当用户输入无法被转换成数字时,parse将会失败,这里采用了跟之前相同的策略,用expect来接收parse返回的Result值,当为Err时则表示类型转换失败,expect会直接将程序crash,而如果为Ok,expect则会获取到转换后到值,并将其赋值给guess。

现在让我们再次运行这段程序:

1
2
3
4
5
6
7
8
9
10
$ cargo run  
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 90
Please input your guess.
20
You guessed: 20
Too small!

现在,我们能够正确执行猜数字的操作了,但是还存在一个问题,就是用户只能够做出一次猜数字的动作,因此我们需要再加入循环的逻辑。

Allowing Multiple Guesses with Looping

loop关键字会启动一个死循环,这能让用户能不断地进行猜数字的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    // --snip--

println!("The secret number is: {secret_number}");

loop {
println!("Please input your guess.");

// --snip--

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

但是此时,用户只能够通过ctrl+c的快捷键或是输入非数字来强制退出程序(例如quit)。而我们所期望的是用户在猜到目标数字时就可以退出程序,因此还需要加上退出这个loop的逻辑。

Quitting After a Correct Guess

我们可以使用break来退出循环:

1
2
3
4
5
6
7
8
9
10
11
12
        // --snip--

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

这样,当用户正确猜中目标数字时就可以直接退出循环了,在这里也就意味着退出程序。

Handling Invalid Input

为了进一步完善游戏的行为,而不是在用户输入非数字时都使程序崩溃,我们让程序忽略用户的非数字输入,这样用户就可以继续进行猜测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// --snip--

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

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

println!("You guessed: {guess}");

// --snip--

由于Result也是一个枚举值,因此可以采用和Ordering类似的方式,使用match表达式来进行处理。值得注意的是,在Err(_)中,_是一个通配符,也就是说我们想匹配所有的Err值,无论它们里面包含的是什么信息。

现在我们的程序应该能够像我们期望的那样运行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ cargo run
Compiling guessing_game v0.1.0 (/path/to/your/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 15
Please input your guess.
1
You guessed: 1
Too small!
Please input your guess.
2
You guessed: 2
Too small!
Please input your guess.
16
You guessed: 16
Too big!
Please input your guess.
15
You guessed: 15
You win!

我们成功地完成了这个猜数字程序,但是还需要再做一个小调整。可以看到,现在程序会将目标随机数打印出来,这是用于调试的,因此我们应该将这一行删掉,最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use rand::Rng;
use std::cmp::Ordering;
use std::io;

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");

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => 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;
}
}
}
}

至此就顺利完成了这个程序,Congratulations!

Summary

我们在编写这个小项目的过程中使用到了很多Rust的新语法,例如let,match等等,目前我们只是匆匆一瞥,大概知道了它们的使用方式,关于更详细的内容我们将在后续的章节中深入地进行学习。

Share 

 Previous post: The Rust Programming Language:Common Programming Concepts Next post: 现代C++32讲:易用性改造 II - 字面量、静态断言和成员函数说明符 

© 2025 John Doe

Theme Typography by Makito

Proudly published with Hexo