Rust Guessing Game
本文大部分内容翻译自:The Rust Programming Language
1. 新建Guessing Game项目
1 | cargo new guessing_game |
编辑Cargo为你创建的src/main.rs
:
1 | use std::io; |
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 | println!("Guess the number!"); |
这段代码打印出一个提示,告诉用户这个游戏是什么,并请求用户输入内容。
2.3. 变量
1 | let mut guess = String::new(); |
这段代码创建了一个变量,用于存储用户的输入。对比下面创建变量的语句:
1 | let apples = 5; |
这段代码创建了一个名为apples
的变量,并将数值5
与它绑定。在Rust中,变量默认是不可变的(immutable),这意味着一旦我们给变量赋值,这个值就不会再改变。
想要创建一个可变的变量,需要在变量名前面加mut
:
1 | let apples = 5; //immutable |
回到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 | io::stdin() |
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)进行编码,它的成员是Ok
和Err
。
Ok
:表示操作成功,其内存放成功生成的值Err
:表示操作失败,其内存放操作失败的原因
Result
类型定义了expect
函数,所以可以在Result
类型的实例上调用expect
。如果Result
的实例是Err
,expect
函数会导致程序崩溃,并显示你向expect
传入的参数中的信息;如果Result
的实例是Ok
,expect
会将Ok
中存放的值取过来返回给你的程序,让你可以使用这个返回值。在本程序中,返回的值是用户输入的字节数。
如果不调用expect
函数,程序依然可以编译,但会显示警告(warning):
Rust发出警告,说你没有使用read_line
函数返回的Result
值,即,程序没有处理可能产生的错误。
2.6. 使用println!的占位符打印
1 | println!("You guessed: {guess}"); |
这段代码打印出包含用户输入的字符串,花括号{}
是占位符,第1对花括号保存格式字符串后面的第1个值,第2对花括号保存格式字符串后面的第2个值,以此类推。在一个println!
语句中打印多个值,可以这样做:
1 | let x = 5; |
这段代码会打印出:x = 5 and y = 10
。
2.7. 运行代码
1 | cargo run |
运行结果:
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 | use std::io; |
首先,我们添加use rand::Rng
。Rng
是一个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. cmp
和Ordering
将main.rs
修改为:
1 | use rand::Rng; |
首先,我们添加了另一个use
语句,将std::cmp::Ordering
这个类型从标准库纳入程序的作用域。Ordering
类型是一个enum
,有3个成员:
- Less
- Greater
- Equal
这3种结果是在比较两个值之后可能得到的结果,然后我们添加了几行使用Ordering
类型的代码。
cmp
方法比较两个值,可以被用在任何可比较的类型上。它会获取一个待比较的值的引用,这里是将guess
与secret_number
比较,然后返回一个Ordering
枚举类型的成员。
我们使用match
表达式和返回的Ordering
的成员来决定如何处理结果。match
表达式由arms
组成,一个arm
由下面几部分组成:
- 一个用于匹配的pattern
- 如果匹配成功,要执行的代码
match
会将Ordering
返回的结果依次与arms
的pattern进行匹配,第1个匹配不成功就继续匹配下一个arm
,若匹配成功则立刻执行该arm
后的代码,不会再往下比较。
4.2. “mismatched types”
但如果执行cargo build
,会发现有编译错误:
显示的错误原因是“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 | use rand::Rng; |
添加的一行代码是:
1 | let guess: u32 = guess.trim().parse().expect("Please type a number!"); |
- shadowing
我们新建了一个变量guess
,Rust允许我们用新的guess
值来屏蔽(shadow)之前的旧值。Shadowing让我们可以重用guess
变量,不需要去创建两个不同的变量(比如guess_str
和guess
)。这个特点在我们想把一个值从一种类型转换成另一种类型时经常用到。 - 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
返回的是Err
,expect
就会使程序崩溃,并打印出我们传入expect
的信息; - 如果
parse
返回的是Ok
,expect
会从Ok
中返回我们想要的数字。
- 如果
- 字符串的
运行程序:
1 | cargo run |
运行结果:
5. 添加功能:使用循环来允许多轮猜数
5.1. loop
关键字
将main.rs
改为如下代码:
1 | use rand::Rng; |
loop
关键字创建了一个无限循环。用户可以不停地猜数,只有通过ctrl + c
才能退出程序,或者通过输入一个非数字,造成程序崩溃,进而退出程序。
5.2. 猜数成功后退出
使用下面代码,在用户成功猜数后退出程序:
1 | use rand::Rng; |
运行结果:
6. 添加功能:处理不合法的输入
当用户输入非数字的字符时,忽略这个输入,让用户继续猜数,而不是崩掉程序:
1 | use rand::Rng; |
这里,我们从调用expect
方法变为使用match
表达式来处理parse
的结果:
- 如果
parse
转换成功,会返回含有转换结果的Ok
,我们将Ok
中的转换结果返回给guess
; - 如果
parse
转换失败,会返回Err
,其中的下划线_
意思是“catchall”,即匹配所有Err
值,不论其中包含的错误原因是什么。进而后面的continue
会被执行,告诉程序继续下一轮循环。
执行结果:
7. 不再打印Secret Number
目前程序会将Secret Number打印出来,将该行println!
去掉,我们的Guessing Game就完成了!
1 | use rand::Rng; |
参考资料
[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