Rust Guessing Game

本文大部分内容翻译自:The Rust Programming Language

1. 新建Guessing Game项目

1
2
cargo new guessing_game
cd guessing_game

编辑Cargo为你创建的src/main.rs

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

2. 代码解析

2.1. use std::io

1
use std::io;

为了得到用户的输入,然后将结果打印出来作为输出,我们需要将io这个输入/输出库纳入作用域。io库来自一个名为std的标准库。Rust在它的标准库中附带了很多东西,如果必须手动导入所有需要的东西,那会非常冗长,但导入很多程序不会使用的东西也是不好的,所以需要取一个平衡。

prelude是Rust自动导入每个Rust程序的内容列表,它尽可能地小,并且专注于几乎在每个Rust程序中都要使用的东西,尤其是traits。

如果你想要用的类型不在prelude中,就需要使用use语句将它明确地纳入程序的作用域。std::io库提供了很多有用的功能,包括接收用户输入的能力。

2.2. fn

1
fn main()
  • fn语法声明了一个函数
  • 括号()中的内容为空,表示函数没有参数
  • 花括号{是函数体的起点

2.3. println! macro

println!是一个macro(宏),用于向屏幕打印字符串。

1
2
println!("Guess the number!");
println!("Please input your guess.");

这段代码打印出一个提示,告诉用户这个游戏是什么,并请求用户输入内容。

2.3. 变量

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

这段代码创建了一个变量,用于存储用户的输入。对比下面创建变量的语句:

1
let apples = 5;

这段代码创建了一个名为apples的变量,并将数值5与它绑定。在Rust中,变量默认是不可变的(immutable),这意味着一旦我们给变量赋值,这个值就不会再改变

想要创建一个可变的变量,需要在变量名前面加mut

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

回到guess game程序,let mut guess引入了一个可变的变量,名为guess。等号=告诉Rust,我们现在想把一个值跟这个变量绑定。=后面的值就是变量guess绑定的值,是调用String::new的结果。

String::new是一个返回String类型的新实例的函数。String是一个字符串类型,由标准库提供,可变长,用UTF-8编码。

::语法表示new是一个与String类型相关联(associated)的函数,这里,new函数创建了一个新的、空的字符串。很多类型都有new函数,因为这是一个创建某一类型新实例的常用名称。

总结来说,let mut guess = String::new();创建了一个可变的变量guess,绑定到了一个新的、空的String类型的实例。

2.4. 获取用户的输入

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

use std::io提供了输入输出功能,上面的代码调用了io模块的stdin函数,这个函数允许我们处理用户的输入。

如果在程序开始没有导入io库,这里可以通过std::io::stdin来调用这个函数。stdin函数返回一个std::io::Stdin类型的实例,是一种表示终端标准输入的句柄(handle)的类型。

.read_line(&mut guess)调用了标准输入句柄的read_line函数来获取用户的输入。我们将&mut guess作为参数传入read_line函数,告诉它要将用户的输入存储到什么字符串里。read_line函数的作用是从标准输入中将用户输入的所有内容拿过来,然后添加到字符串后面(不会覆盖字符串中原有的内容)。我们传入的字符串变量需要是可变的,这样read_line函数才能修改字符串中的内容。

&表示传入的参数是一个引用(reference)。引用能够让代码中多个地方去访问同一块数据,但不需要将那块数据多次复制到内存中。与变量类似,默认情况下引用也是不可变的,因此需要写&mut guess而不是&guess,这样才能让引用时可变的。

2.5. 处理潜在的Failure

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

read_line函数除了将用户输入的内容存储到我们传入的字符串之外,还会返回一个Result类型的值。Result是枚举类型(enum),是一种拥有多种可能状态的类型,每一种可能的状态称为成员(variant)。

Result类型的作用是对错误处理信息(error-handling message)进行编码,它的成员是OkErr

  • Ok:表示操作成功,其内存放成功生成的值
  • Err:表示操作失败,其内存放操作失败的原因

Result类型定义了expect函数,所以可以在Result类型的实例上调用expect。如果Result的实例是Errexpect函数会导致程序崩溃,并显示你向expect传入的参数中的信息;如果Result的实例是Okexpect会将Ok中存放的值取过来返回给你的程序,让你可以使用这个返回值。在本程序中,返回的值是用户输入的字节数。

如果不调用expect函数,程序依然可以编译,但会显示警告(warning):
不调用expect显示警告

Rust发出警告,说你没有使用read_line函数返回的Result值,即,程序没有处理可能产生的错误。

2.6. 使用println!的占位符打印

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

这段代码打印出包含用户输入的字符串,花括号{}是占位符,第1对花括号保存格式字符串后面的第1个值,第2对花括号保存格式字符串后面的第2个值,以此类推。在一个println!语句中打印多个值,可以这样做:

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

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

这段代码会打印出:x = 5 and y = 10

2.7. 运行代码

1
cargo run

运行结果:
运行guess game

3. 添加功能:生成Secret Number

下面,我们要生成一个secret number让用户来猜,这个secret number每次都会变化。目前,Rust的标准库没有提供随机数功能,但名为rand的crate提供了该功能。

3.1. 使用Crate获取更多功能

Crate是许多Rust源文件的集合,guessing game这个工程是一个可执行文件,即binary crate;rand crate是一个library crate,包含可被其它程序使用的代码,但它自己是不能运行的。

在将rand使用到代码中之前,我们需要修改Cargo.toml文件,把rand crate作为依赖引入工程。

打开Cargo.toml,将下面一行代码添加到[dependencies]下面:

1
rand = "0.8.3"

注意要使用一模一样的名称rand和版本号0.8.3,否则代码样例有可能出问题。

Cargo能够识别Semantic Versioning(一个写版本号的标准),0.8.3^0.8.3的缩写,表示版本号至少是0.8.3,但低于0.9.0

下面,使用cargo build重新构建项目。

3.2. 生成随机数

修改main.rs为如下内容:

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::RngRng是一个trait,定义了随机数生成器实现的方法(methods)。


trait
trait告诉Rust编译器某个特定类型拥有可能与其他类型共享的功能,可以通过trait以一种抽象的方式定义共享行为,类似于其他语言中的接口(interface)功能,但有一些不同。

trait可以被类型继承,它只能由3部分组成:

  • functions(方法)
  • types(类型)
  • constants(常量)

所有trait都定义了一个隐含类型Self,指向实现了该trait的类型,类型需要通过独立的implementations去实现不同的trait。

trait中可以提供与它关联的条目(类型别名的实际类型、函数的函数体、常数的值表达式)的实际定义,但不是必须的。

  • 如果trait提供了定义,该定义即为任何实现它的类型的默认行为(如果对应类型没有override的话);
  • 如果trait未提供定义,任何实现它的类型都必须提供一个定义。

rand::thread_rng函数提供了可用的特定随机数生成器,它是一个当前执行线程的本地生成器,由操作系统提供种子(seed)。

然后我们在随机数生成器上调用gen_range方法(method),这个方法由 Rng trait定义,我们使用use rand::Rng语句将它引入程序的作用域。

gen_range方法将范围表达式(range expression)作为参数来生成该范围内的随机数。这里使用的范围表达式的格式是start..=end,取左右边界闭区间,所以我们需要将它写成1..=100来请求生成一个1~100之间的随机数。

使用cargo run来运行程序。

4. 添加功能:将Guess与Secret Number比较

4.1. cmpOrdering

main.rs修改为:

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

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

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类型是一个enum,有3个成员:

  • Less
  • Greater
  • Equal

这3种结果是在比较两个值之后可能得到的结果,然后我们添加了几行使用Ordering类型的代码。

cmp方法比较两个值,可以被用在任何可比较的类型上。它会获取一个待比较的值的引用,这里是将guesssecret_number比较,然后返回一个Ordering枚举类型的成员。

我们使用match表达式和返回的Ordering的成员来决定如何处理结果。match表达式由arms组成,一个arm由下面几部分组成:

  • 一个用于匹配的pattern
  • 如果匹配成功,要执行的代码

match会将Ordering返回的结果依次与arms的pattern进行匹配,第1个匹配不成功就继续匹配下一个arm,若匹配成功则立刻执行该arm后的代码,不会再往下比较。

4.2. “mismatched types”

但如果执行cargo build,会发现有编译错误:
比较guess与secret number的结果

显示的错误原因是“mismatched types”,Rust不能将一个字符串跟一个数字进行比较。

Rust会做类型推断。当我们写let mut guess = String::new()的时候,Rust能够推断出guess应该是一个String类型,而secret_number是一个数字类型。


Rust的一些数字类型:

  • i32:32-bit数字
  • u32:无符号32-bit数字
  • i64:64-bit数字

等等。


除非具体说明,Rust默认的数字类型是i32,也正是这里secret_number的类型。

我们需要将String类型的guess转换成数字类型才能将它与secret_number比较:

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

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

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

添加的一行代码是:

1
let guess: u32 = guess.trim().parse().expect("Please type a number!");
  • shadowing
    我们新建了一个变量guess,Rust允许我们用新的guess值来屏蔽(shadow)之前的旧值。Shadowing让我们可以重用guess变量,不需要去创建两个不同的变量(比如guess_strguess)。这个特点在我们想把一个值从一种类型转换成另一种类型时经常用到
  • trim
    新变量guess被绑定到了表达式guess.trim().parse()上。guess.trim().parse()中的guess指的是原先的guess变量,作为字符串来接收用户的输入。trim方法会将一个String类型的实例去掉首尾的whitespaces。想要将一个字符串转换成u32,这是必须要做的,因为被转换的字符串中只能有数字。
  • parse
    • 字符串的parse方法将字符串转换成其它类型。这里,我们用parse将字符串转换成数字。
    • let guess: u32告诉Rust我们想要转换成的类型是什么,:告诉Rust我们将在后面注明这个类型。这里我们使用的是无符号32-bit整数,即u32。将u32类型的数字与另一个数字secret_number作比较,会让Rust推断secret_number应该也是u32类型的。所以现在,comparison会发生在两个有同样类型的变量之间。
    • 返回Result类型:由于被转换的字符串中可能含有非数字,就会造成错误,因此parse会返回Result类型。我们同样使用expect方法来处理这个Result类型的返回值。
      • 如果parse返回的是Errexpect就会使程序崩溃,并打印出我们传入expect的信息;
      • 如果parse返回的是Okexpect会从Ok中返回我们想要的数字。

运行程序:

1
cargo run

运行结果:
解决“mismatched types”错误之后的运行结果

5. 添加功能:使用循环来允许多轮猜数

5.1. loop关键字

main.rs改为如下代码:

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

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

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

loop关键字创建了一个无限循环。用户可以不停地猜数,只有通过ctrl + c才能退出程序,或者通过输入一个非数字,造成程序崩溃,进而退出程序。
无限循环猜数

5.2. 猜数成功后退出

使用下面代码,在用户成功猜数后退出程序:

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
36
37
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);

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

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;
}
}
}
}

在打印出“You win!”之后,添加`break`,可以让程序跳出循环。

运行结果:
猜数成功后退出

6. 添加功能:处理不合法的输入

当用户输入非数字的字符时,忽略这个输入,让用户继续猜数,而不是崩掉程序:

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
36
37
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);

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

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;
}
}
}
}

这里,我们从调用expect方法变为使用match表达式来处理parse的结果:

  • 如果parse转换成功,会返回含有转换结果的Ok,我们将Ok中的转换结果返回给guess
  • 如果parse转换失败,会返回Err,其中的下划线_意思是“catchall”,即匹配所有Err值,不论其中包含的错误原因是什么。进而后面的continue会被执行,告诉程序继续下一轮循环。

执行结果:
添加错误处理

7. 不再打印Secret Number

目前程序会将Secret Number打印出来,将该行println!去掉,我们的Guessing Game就完成了!

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;
}
}
}
}

参考资料

[1] The Rust Programming Language:https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html
[2] Module std::prelude: https://rust.ffactory.org/std/prelude/index.html
[3] Rust 程序设计语言 简体中文版:https://kaisery.github.io/trpl-zh-cn/ch10-02-traits.html
[4] Rust:Trait:https://zhuanlan.zhihu.com/p/127365605