1. 编写一个猜字谜游戏
本文我们将会用 Rust
语言实现出一个经典的初学者的编程练习:猜字谜游戏。具体内容如下:
(1) 程序会随机产生一个介于 1 - 100
之间的整数;
(2) 开始提示玩家输入猜测的数字;
(3) 程序提示输入的猜测值过高还是过低,若猜测正确则打印祝贺信息并退出游戏,反之继续。
Let’s do it
2. 创建新项目
在学习新内容时候,切忌忘记前面学习的内容。现在,使用 Cargo
创建我们的新项目。
imaginemiracle:rust_projects$ cargo new guessing_game
Created binary (application) `guessing_game` package
imaginemiracle:rust_projects$ cd guessing_game/
imaginemiracle:guessing_game$ ls
Cargo.toml src
当然我们是可以直接运行新创建的项目的,使用 cargo run
。
imaginemiracle:guessing_game$ cargo run
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/guessing_game`
Hello, world!
3. 开始初次编写
当使用 cargo new <项目名>
创建一个新的项目时,我们只需要修改其 src
目录下的源文件即可,这里我们不需要太多的文件,只用到一个 main.rs
即可。
imaginemiracle:guessing_game$ vim src/main.rs
3.1. 先处理用户的输入
游戏要求玩家输入一个数字,并判断。这里我们先完成让玩家输入和获取玩家输入的数字这个小功能。
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);
}
3.2. 代码说明——01
先来看上述代码的第一行。由于游戏需要获取用户的输入并将结果打印到屏幕,因此这里就需要使用到标准库中的 io
输入/输出,即 std
标准库中的 io
,将其包含进此程序即可。
use std::io;
看到这里是否觉得与 c++
中的 using namespace
很是相似,其用法也比较类似,我们这里不讨论 c++
,提及该话题只是为了更快的熟悉和接受这种使用方法。
事实上在默认情况下,Rust
会将标准库中的一部分定义,引入每一个程序的使用范围,也就是默认包含了这些。在 Rust
里这些被默认引入的标准库被称作 Prelude。(为了便捷,引入了几乎所有程序都使用到的库。为了精简,只引入一小部分几乎所有程序都需要使用的库。——在便捷和冗余之间寻求 balance
)
若需要使用的库不在 Prelude
中,则需要手动的显示引入,需要使用到关键字 use
。
3.2.1. 函数声明
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
如果看过《小小白的入门学习》前几篇,那么这三行代码应该会完全没有疑点。
关键字 fn
在这里声明了一个名为 main
的新函数,()
其中没有内容,则表示该函数不带参数,{
表示着 main
函数函数体的开始。
如下两行则使用 println!
宏将字符串打印到终端屏幕上。从文字意思可以看出,要开始一个什么游戏,并且还需要自己输入。
3.2.2. 变量定义
到了这里发现需要玩家输入一个数值,因此我们需要有一个变量来保存玩家的输入。
let mut guess = String::new();
使用关键字 let
创建了一个名为 guess
的变量,运算符 =
则表示我们需要为该变量绑定一些东西。=
的右侧便是绑定的具体值,这里为 String::new()
,该函数会返回一个 String
的实例。String 是标准库提供的字符串类型。
::
是 Rust
中的一种语法,String::new()
表示 new()
是 String
类型的关联函数。关联函数是在该类型上实现的具体函数 (具体定义可以查看 String源码的382行)。new
函数会创建一个新的空字符串。
关键字 mut
的存在,表示该变量是可变的变量。因为在 Rust
中,声明的变量默认情况下是不可变的(如下),而这部分内容我们将在后面的文章里详细的描述,这就是为什么最后才对 mut
关键字作解释的原因。
let apples = 5; // 变量不可变 (immutable)
let mut bananas = 5; // 变量可变 (mutable)
综上所述,let mut guess = String::new();
这行代码创建了一个可变的变量,并将该变量绑定了一个空的新 String
实例。
3.2.3. 获取标准输入
OK!我们已经准备好了一个变量用来存储玩家的输入了,是时候该让玩家做点什么了(or 是我们需要为玩家做点什么)。
记得在第一行就已经引入了标准库的 io
,该她出场了!
io::stdin()
.read_line(&mut guess)
[注]: 细心的小伙伴会发现这并不是一个完整的语句,因为没有 ‘;’ ,别急这里暂时先看她的前半段。
可以通过调用标准库中的 stdin
函数用来处理玩家的输入,接下来的 .read_line(&mut guess)
通过调用 read_line 标准输入句柄 (stdin
) 中的函数来获取玩家的输入到具体变量中 (mut guess
)。符号 &
表示引用,有关引用的含义这里和 c++
中的引用是相同的。引用本身不会分配内存,而是直接指向其引用的内存空间,这样使用的好处就是代码在多处访问一个数据时,无需多次拷贝,而直接将数据复制到变量对应的内存中。在这里暂时不需要清楚太多的细节,因为 Rust
在这方面已经实现的完善了,你可以非常安全而容易的去使用即可。需要注意的是,这里传入的参数是 &mut guess
而非 & guess
,两者的书写区别在于变量是否可变,这是很多初学者都会犯的错误。
由于我们在程序的开头引入的 std::io
,这里则可以直接使用 io::stdin()
,stdin()
函数会返回一个 struct std::io::Stdin 类型的实例。
如果在开始忘记引入 std::io
,那么在这里则可以这样使用 std::io::stdin()
,是一样的。
3.2.3.1. 有关 read_line() 的小知识
通过代码以及上文描述,我们已经清楚了 read_line(&mut guess)
函数会调用标准输入句柄 (stdin
) 中的 read_line()
函数将玩家输入的内容保存到 mut guess
中。不仅如此,read_line
会将标准输入的任何内容追加到字符串后(不会覆盖原有内容)。看下面这个例子便会清楚。
use std::io;
fn main() {
println!("read_line test.");
let mut str = String::new(); // 定义一个 String 类型的可变变量
println!("Please input something.");
// 从标准输入获取内容并添加到 mut str
io::stdin()
.read_line(&mut str)
.expect("Failed to read line.");
// 输出 str 的内容
println!("str = {}", str);
println!("Please input something again.");
// 再次从标准输入获取内容并添加到 mut str
io::stdin()
.read_line(&mut str)
.expect("Failed to read line.");
// 输出 str 的内容
println!("str = {}", str);
}
上面这个例子的输出如下:
imaginemiracle:read_line$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/read_line`
read_line test.
Please input something.
Hello world # 第一次的输入
str = Hello world # 第一次的输出
Please input something again.
I'm ImagineMiracle. # 第二次的输入
str = Hello world # 第二次的输出
I'm ImagineMiracle. # 第二次的输出
可以清楚的看到 str
里的内容,在调用两次 read_line
获取标准输入的内容后,全部都会追加到 mut str
中,并不会覆盖原有内容。
3.2.4. 故障处理——Result
Now! 我们来看这条语句的最后一部分。
.expect("Failed to read line.");
[注]: 一般当一条语句或表达式较长,则应该合理的使用‘换行符’和‘空格’将一条长的语句分解,这样是明智的做法,这将会帮助你或其他人更容易的阅读你的代码。
read_line()
会将标准输入的所有内容都放入传递给她的 String
中,并且给我们返回一个值。在此处的返回值类型为 io::Result(Rust
在其标准库汇总中定义了多种类型的 Result
)。
Result
的变体是 Ok
和 Err
。Ok
代表操作执行成功,反之 Err
则表示操作失败,并包含操作失败的信息,供调试使用。
而 io::Result
的实例可以调用她自己的 expect 函数。如果 io::Result
返回的是 Err
,expect
则将导致程序崩溃的信息作为参数传递给 expect
。(当返回 Err
时,一般会是操作系统底层发生错误信息)。若返回值为 Ok
,那么在本文程序中 Ok
所持有的返回值便是输入的字节数。
[注]: 若不使用 ‘expect’ 程序可以正常通过编译,但编译器会发出警告!
imaginemiracle:guessing_game$ cargo build
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | / io::stdin()
11 | | .read_line(&mut guess);
| |_______________________________^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
由于没有使用 read_line
的返回值 Result
,这里 Rust
会发出警告,表示程序没有处理可能发生的错误。
为了没有警告,正确做法应该是在编写过程中添加好每一个故障处理,而在本例中只想在出现问题时让这个程序崩溃,因此直接使用 expect
即可。在之后将会更多的了解到故障处理以及恢复。
3.2.5. println! 使用占位符打印值
对于如下代码,虽然还只是刚开始学习 Rust
,相信看了之后也会明白其中每个符号的作用是什么。
println!("You guessed: {}", guess);
该行代码会打印出包含玩家输入的字符串。这里的 {}
是一个占位符,表示这里将会输出一个由其它什么来表示的值,而确定她的具体值是由 ,
后面的变量决定,他们是按照顺序的规则来一一对应。当需要打印多个值时,只需要按照 {}
,和排列好与其对应的变量即可。
fn main() {
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
}
该段程序将会输出 x = 5 and y = 10
。
4. 测试编写的第一部分功能
上述代码已经能够实现该游戏的一小部分功能了,现在来测试她是否能正常工作——使用 cargo run
。
imaginemiracle:guessing_game$ cargo run
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
22
You guessed: 22
已经完成的功能:
(1) 让玩家输入数字;
(2) 获取输入的数字,并打印出来。
5. 编写第二部分功能——产生神秘数字(随机数)
该游戏需要生成一个秘密的数字,让玩家来猜。这个数字应该每次的不同,或者说是没有规律的出现,这样才有意义。我们将数字的产生范围规定到 1-100
,这样不至于太难猜。在 Rust
标准哭中目前还没有随机数产生函数。不过 Rust
团队提供了一个可以产生随机数的 rand crate。可以看到目前 rand
的版本已经更新到 0.8.5
。
5.1. 使用 crate 将可使用更多函数
一个 crate
是 Rust
源文件的集合。我们正在构建的这个小游戏项目也将会生成一个二进制 crate
,它是一个可执行文件。Rust
中,crate
是一个独立的可编译单元。具体说来,就是一个或一批文件(如果是一批文件,那么有一个文件是这个 crate
的入口)。它编译后,会对应着生成一个可执行文件或一个库。
[注]: crate 是用于包含在其他程序中的代码,并不能单独执行。
当我们的程序使用 rand
之前,应该先修改 Cargo.toml
文件,将 rand crate
作为依赖项包含进去。打开 Cargo.toml
文件,并在 [dependencies]
下添加即可。注意版本号,尽量使用最新版。
# File: Cargo.toml
[dependencies]
rand = "0.8.5"
在 Cargo.toml
中的 [dependencies]
会告诉你的 Cargo
项目依赖于哪些外部 crates
以及需要这些 crates
的哪些版本。在这种情况下,我们使用 rand
语义版本说明符来指定 crate 0.8.5
。Cargo
通过语义版本控制去识别(也称作 SemVer
),这是编写版本号的标准。该数字 0.8.5
实际上是 ^0.8.5
的简写,这意味着版本至少是 0.8.5
但低于 0.9.0
的任何版本。Cargo
认为这些版本具有与 0.8.5
版本兼容的公共 API
,此规范将确保使用最新的补丁版本与被依赖的代码一起编译。但不保证其它版本像 0.9.0
或更高版本都具有与本文代码使用的相同的 API
。
现在我们以及将 rand crate
包含进去,现在不要对 main.rs
做任何修改,直接构建本项目查看一下有什么不同。
imaginemiracle:guessing_game$ cargo build
Updating crates.io index
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3构建
Downloaded ppv-lite86 v0.2.16
Downloaded getrandom v0.2.6
Downloaded rand v0.8.5
Downloaded cfg-if v1.0.0
Downloaded libc v0.2.126
Downloaded 7 crates (773.0 KB) in 1.22s
Compiling libc v0.2.126
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.6
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1m 04s
[注]: 如果这里提示 ”error: failed to get ’rand‘“,尝试将 ’rand‘ 版本修改为 0.8.4 再做构建。
这里各位的输出可能各有不同(版本号不同,但都会与代码兼容),输出的顺序也可能不同(取决与操作系统)。但是只要看到能下载成功并完成(提示 Finished
),则说明依赖构建成功,在程序中将可以直接使用包含进去的 crate
中提供的方法。
当我们包含一个外部依赖项时,Cargo
会从注册表中获取依赖项所需的所有内容的最新版本,该注册表是来自 Crates.io 的数据副本。Crates.io
是 Rust
生态系统中的人们发布他们的开源 Rust
项目并供其他人使用的地方。
更新注册表后,Cargo
会检查该 [dependencies]
部分并下载列出的所有尚未下载的 crate
。在这种情况下,虽然我们只列出 rand
了一个依赖项,但 Cargo
还抓取了其他 rand
依赖于工作的 crate
。下载 crates
后,Rust
编译它们,然后使用可用的依赖项编译项目。
如果立即再次运行 cargo build
而不进行任何更改,则除了该 Finished
行之外,将不会得到任何输出。Cargo
知道它已经下载并编译了依赖项,并且 Cargo.toml
文件被修改。Cargo
也知道 src
目录下的源文件没有修改,所以它也不会重新编译。无事可做,它只是退出。如果只是修改了源文件,则只会编译源文件中相关代码,不会再重复下载和编译 crate
。
5.2. 生成随机数
接下来,我们将会使用到刚被添加进依赖项的 crate
中提供的 rand
函数。让我们更新一下 src/main.rs
。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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);
}
5.3. 代码说明——02
5.3.1. 引入 trait rand::Rng
首先,我们添加了一行 use rand::Rng
。该 Rng
特征 (trait
) 定义了随机数生成器实现的方法,并且该特征必须被引入在我们使用这些函数的范围内。这里对于 trait
不做过多的讨论,后面再深入学习。
5.3.2. 定义变量并产生随机数
首先定义看一个名为 secert_number
的不可变变量,并且绑定了一个随机数给它。这里调用了 rand::thread_rng
来提供将要使用的特定随机数生成器的函数:一个位于当前执行线程的本地并由操作系统确定种子的函数。然后再调用 gen_range
随机数生成器上的方法。该函数由我们通过 use
语句引入范围的 Rng
特征定义。use rand::Rng
该 gen_range
方法将范围表达式作为参数,并在该范围内生成一个随机数。在这里使用的范围表达式的形式 start..end
是包含下限但不包含上限,因此需要指定为 1..101
,表示请求一个介于 1
和 100
之间的数字。或者,可以传递范围1..=100
,这是等价的。
(种子: 是关于随机数取值的一个关键数值。由于随机数的也是根据固定算法来产生的,虽然在一次执行,确实会发现数据产生是随机的,但再次执行程序会发现两次产生的数据序列相同。这是因为随机数函数的种子值相同导致,因此为了实现真正的随机,需要通过’种子‘来改变。一般默认情况是使用当前系统时间作为种子,由于每一时刻的时间值都不同,也就实现了真正意义上的随机)。
注意: 你也许不清楚要使用哪些特征以及从 crate 调用哪些方法和函数,因此每个 crate 都有包含使用说明的文档。Cargo 的另一个巧妙功能是运行该 cargo doc --open 命令将在本地构建所有依赖项提供的文档并在浏览器中打开它。例如,如果对 rand crate 中的其他功能感兴趣,请运行cargo doc --open并单击rand左侧边栏中的。
6. 测试随机数功能
可以看到在源文件中还添加了一行 println!
宏,将产生的随机数打印了出来,这只是作为调试来用。在真正的游戏中,不会让玩家提前知道答案的吧!Let’s run it.
可以多运行几次,查看每次产生的数值是否是随机的。
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 35.
Please input your guess.
12
You guessed: 12
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 54.
Please input your guess.
12
You guessed: 12
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 17.
Please input your guess.
12
You guessed: 12
已经完成的功能:
(1) 产生随机数,并打印出来;
(2) 让玩家输入数字;
(3) 获取输入的数字,并打印出来。
7. 编写第三部分功能——比较数值
到这里我们已经获得了玩家的输入,以及随机产生的秘密数字。
Girl: 那我们是不是可以直接比较了呢?
Boy: 为什么这样问?难道不是吗?
Girl: 先试试看吧。
Boy: ???
use std::io; // 引入标准库的输入输出
use rand::Rng; // 引入定义了随机数生成函数的trait
use std::cmp::Ordering; //引入用于数值比较的函数
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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!("Yeah! You win!"),
}
}
7.1. 代码说明——03
7.1.1. 引入 std::cmp::Ordering
首先,我们添加另一个 use
语句,将标准库中调用的类型 std::cmp::Ordering
引入我们的代码范围。该 Ordering
类型也是一个枚举,具有变体 Less
、Greater
和 Equal
(前者小于后者、前者大于后者、前者等于后者)。这是比较两个值时可能出现的三种结果。
然后我们在下面添加了几行,用于描述遇到该 Ordering
的几种情况需要做的事情。cmp
函数可以比较两个值,并且可以在任何可以比较的东西上调用。它比较你想要比较的任何内容:这里为 guess
与 secret_number
进行比较。然后会返回 Ordering
的变体,在该行的前面有个关键字 match
表示这是一个 match
表达式,其格式为:
match number_01 {
case_01 => to do,
case_02 => to do,
......
}
match
表达式会匹配其后 {}
中的值,若匹配成功则执行所指向的语句。有关其详细的使用将会在后面文章汇总进行学习。
这里 match
通过匹配返回的 Ordering
的枚举变体,来决定两者数值的关系,以及之后该输出什么。
Boy: 这应该算写好了吧!
Girl: ~,编译一下看看吧。
imaginemiracle:guessing_game$ cargo build
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
|
= note: expected reference `&String`
found reference `&{integer}`
error[E0283]: type annotations needed for `{integer}`
--> src/main.rs:8:44
|
8 | let secret_number = rand::thread_rng().gen_range(1..101);
| ------------- ^^^^^^^^^ cannot infer type for type `{integer}`
| |
| consider giving `secret_number` a type
|
= note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
- impl SampleUniform for i128;
- impl SampleUniform for i16;
- impl SampleUniform for i32;
- impl SampleUniform for i64;
and 8 more
note: required by a bound in `gen_range`
--> /home/imaginemiracle/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.5/src/rng.rs:131:12
|
131 | T: SampleUniform,
| ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
|
8 | let secret_number = rand::thread_rng().gen_range::<T, R>(1..101);
| ++++++++
Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors
Boy: Wo Wo~,これは何ですか!!
Girl: Ah Oh!! We get some error.
7.1.2. Rust 里的整型——integer
我们来看一下上面的错误,首先看第一个错误,发生在第 22
行,提示我们 “类型不匹配”。奥!原来 guess
是一个 integer
而 secret_number
是个 String
,Rust
提示这两个无法比较,因此需要在 guess
和 secret_number
这两个变量的类型一致的情况下才可以进行比较。
Rust
有一个强大的静态类型系统。但是,它也有类型推断。当我们编写这条语句 let mut guess = String::new()
,Rust
能够推断出 guess
应该是一个 String
类型,并且没有提示让我们添加类型声明。而 secret_number
, 是整数类型。Rust
的一些整数类型的值可以在 1
到 100
之间:
i32
是一个32
位有符号整数;u32
,表示一个无符号的32
位整数;i64
, 表示一个64
位有符号整数;u64
,表示一个64
无符号整数。
除非另有说明,否则 Rust
默认的的整数类型为 i32
,而这里的 secret_number
并未添加类型说明,因此 Rust
推断出她是一个整数类型 u32
。所以错误的原因是 Rust
无法比较字符串和数字类型。
7.1.3. 类型转换——String to integer
那么我们要做的是,统一这两个变量的类型,即只需要将 String
类型的 guess
转换为整数类型。来看看怎么做:
// ------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!("Yeah! You win!"),
}
}
关键代码:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
我们将这个新定义的变量 guess
绑定到表达式 guess.trim().parse()
。表达式中的是指包含输入作为字符串 guess
(原来的 guess
)。guess
实例上的 trim
方法 String
将消除开头和结尾的所有空格或回车。
[注]: 空格和回车的 ASCII 表示符分别为 '\r' '\n。'
Boy: 为啥子要去掉空格,说的到底她里面有空格吗?
Girl: 呀呀!仔细回想一下在执行这段程序后,在输入时,是否按过回车?
Boy: enmm,这是必须做的啊,否则程序无法知道我们是否输入完成。这个也算!?
Girl: Bingo,假如你输入的是 28
,在之后按完回车后。事实上 read_line
会将换行符也纳入其中,也就是说 read_line
看到的字符串是这样的 28\n
,因此得去掉这个小尾巴,我们必须这样做,才能得到只包含了数字的字符串,在之后的转换数字和比较时才不会出现问题。
Boy: 啊,这样子哦,那这一次就听你的吧!
字符串上的 parse
函数会将字符串解析为某种数字类型。因为这个函数可以解析多种数字类型,所以我们得根据需要来告诉 Rust
我们想要的确切数字类型 let guess: u32
。后面的冒号 ( :
),告诉 Rust
我们需要将 guess
变量的类型定义为指定的类型 u32
。所以现在比较将在相同类型的两个值之间进行!
Boy: ちょっと待って、これは大丈夫か?
Girl: どうしたの?
Boy: だって、どちらも「guess」という名前です。
Good question。確(たし)かに(确实如此),看起来我们是定义了两个相同名的变量,但这在 Rust
中是被允许的。Rust
允许用一个新的值来覆盖之前的值。在 Rust
我们可以重复使用变量名,用新的变量将旧的覆盖,依然是之存在了一个名为 guess
的变量。Rust
不会强迫我们创建两个唯一的变量。后面的文章里将详细对此进行介绍,现在只需要知道,一般当希望将变量从一种类型转换为另一种类型时,通常会使用此功能。
该parse方法仅适用于可以逻辑转换为数字的字符,因此很容易导致错误。例如,如果字符串包含 A👍%
这类非数字字符,则无法将其转换为数字。因为它将会发生错误,所以该parse
方法返回一个 Result
类型,就像该 read_line
方法所做的一样(在前面的“故障处理” Result
中讨论过)。我们将再次使用该 expect
方法以同样的方式处理 Result
。如果由于无法从字符串中创建数字而 parse
返回一个 Err
变体,则该调用( expect
)将使游戏崩溃并打印我们给它的错误信息。如果可以成功地将字符串转换为数字,它将返回的变体 Ok
, 并将 expect
从值中返回我们想要的数字。
8. 测试数值比较功能
现在让我们运行一下修改后的代码吧!
imaginemiracle:guessing_game$ cargo run
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.65s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 73.
Please input your guess.
23
You guessed: 23
Too small!
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 66.
Please input your guess.
80
You guessed: 80
Too big!
细心的朋友会发现这里运行了两次,第一次在输入的时候直接输入了数字和回车,而在第二次执行后,是先输入了一些空格,然后在输入数字和回车,程序仍然会正确的获取到数字。并且会提示出我们输入的数字过小或是过大。
已经完成的功能:
(1) 产生随机数,并打印出来;
(2) 让玩家输入数字;
(3) 获取输入的数字,并打印出来;
(4) 比较产生的随机数和玩家输入的数值,并打印出与秘密数字的对比结果。
哇唔!我们已经完成了这个游戏的大部分功能啦!続けましょう。
9. 编写第四部分功能——循环多次输入猜测
现在玩家只能猜测一次,无论是否猜对都会退出,真正的游戏应该不会这么无趣吧!让我们使用循环来让玩家可以多次猜测直到正确才退出游戏。
use std::io; // 引入标准库的输入输出
use rand::Rng; // 引入定义了随机数生成函数的trait
use std::cmp::Ordering; //引入用于数值比较的函数
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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!("Yeah! You win!");
break;
},
}
} // End of loop
}
9.1. 代码说明——04
9.1.1. 添加循环——loop
Rust
里的循环:
loop {
...
...
}
上述代码的功能等同于 c/c++
中的 while(1)
while(1) {
...
...
}
关键字 loop
标识着后面的 {}
描述的是一个循环体,{}
中的具体代码将会循环的执行。
9.1.2. 退出循环——break
是的,我们可以找到合适的位置添加了循环,可以让玩家玩到痛快,可,,,如何处理正确结果时的退出循环呢?
很简单,只需要在 loop
的循环体遇到 break
语句时,便会退出循环,问题是我们该写在哪里呢?
这个时候在循环体中的 match
表达式便发挥了它另外的作用,当 match
匹配到相应的结果后,不仅只可以执行一条语句,它是允许执行多条语句的,只需要简单的将他们用 {}
括起来,就变成了一组语句,match
匹配到该项后,会将 {}
内的语句全部执行,很方便对不对。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("Yeah! You win!");
break;
},
}
像这样,当然只有一条语句的时候也可以使用 {}
括起来,但需要注意的是,在 {}
的每条语句都需要认真的写好他们的结束符号哦 ;
。千万别忘记,这一点很重要。
10. 测试循环&退出
让我们来测试一下新添加的功能吧!
imaginemiracle:guessing_game$ cargo run
Compiling guessing_game v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 13.
Please input your guess.
16
You guessed: 16
Too big!
Please input your guess.
12
You guessed: 12
Too small!
Please input your guess.
13
You guessed: 13
Yeah! You win!
Boy: 哇唔!成了!
Girl: 现在高兴还太早哦!
Boy: 嘿哈!这次我知道,我们还没删掉那行打印了秘密数字的输出,对不对 (得意^-^)。
Girl: 可不仅仅是这样唔。
Boy: 唏。。故弄玄虚!
Girl: 可千万不要不信我哦!要不要试试输入一下其它非数字的字符看看结果呢?
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 33.
Please input your guess.
op by boy
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:22:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
哇哇哇!发生了不得了的事情,这么简单就让我们写的程序崩溃掉,这可还行?
11. 处理无效输入
让我们完善我们的代码吧!首先我们需要做的是,让我们的程序忽略掉玩家的胡闹 (无效输入),但是也不能简单的遇到无效输入时直接退出程序,而是应该让调皮的他们重新输入。
我们可以通过修改转换 guess
类型这行代码来实现我们的想法。
// ---snip---
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line.");
//let guess: u32 = guess.trim().parse().expect("Please type a number!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
// ---snip---
11.1. 代码说明——05
我们从一个 expect
的调用变成一个 match
表达式,从一个错误崩溃转移到处理错误。注意 parse
函数会返回一个 Result
类型的实例,它具有 Result
的变体枚举 Ok
和 Err
我们可以好好利用这一点,就像之前我们使用 cmp
函数处理 Ordering
一样。
如果 parse
能够成功地将字符串转换为数字,它将返回一个 Ok
( Result
的变体 ) 同时包含结果转换后的具体数值。此时 match
将会匹配到第一个 Ok
这例情况,并且 match
表达式会将返回的 num
值作为参数放进 Ok
的形参 (即 Ok
将会包含该数值),而指向的执行的语句只有一个值 num
,即转换后的数值。那么此时的整条语句就变成了这样,let guess: u32 = num;
即相当与在这个时候给 guess
赋值。
如果无法 parse
将字符串转换为数字,它将返回一个 Err
( Result
的变体 ) 包含有关错误的更多信息的值。此时 match
将会匹配到第二个 Err
这例情况。这里的 _
( Err
中的参数,下划线),它类似一个通配符,代表着传过来的所有信息,即无论转换的是什么字母或符号,不管转换后是什么,通通传给 Err
。匹配到这里,即代表玩家输入了无效的字符,我们应该告诉程序忽略它,并要求玩家重新输入,即重入循环。在代码中很简单,使用一个 continue
语句即可。
12. 测试处理无效输入
让我们测试一下我们新加的功能。
imaginemiracle:guessing_game$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 71.
Please input your guess.
hello
Please input your guess.
i'm imaginemiracle
Please input your guess.
hahahahaha
Please input your guess.
71
You guessed: 71
Yeah! You win!
现在看来我们的游戏功能都在按预期在进行了。
13. 猜字谜游戏——完成
别忘记小男孩的提醒哦,我们还没删除那将谜底告知天下的输出呢!(但是这个输出在我们测试时是非常有用的)
让我们删除那行输出,就得到了完整的游戏啦!
use std::io; // 引入标准库的输入输出
use rand::Rng; // 引入定义了随机数生成函数的trait
use std::cmp::Ordering; //引入用于数值比较的函数
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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!("Yeah! You win!");
break;
},
}
} // End of loop
}
到此,恭喜我们成功构建了猜字谜游戏!
附1. 确保项目可重建的 Cargo.lock 文件
Cargo
有一种机制,可确保每次自己或其他任何人构建代码时都可以使用相同的组建来重建:Cargo
将仅使用开发者指定的依赖项的版本,直到开发者另有说明。例如,假设不久后版本 0.8.6
的rand crate
会发布,并且该版本包含一个重要的错误修复,但它也可能因此会破坏自己的代码。为了处理这个问题,Rust
在第一次运行 cargo build
时创建了Cargo.lock
文件,所以我们现在在guessing_game
目录中有这个文件 。
imaginemiracle:guessing_game$ ls
Cargo.lock Cargo.toml src target
当第一次构建项目时,Cargo
会找出符合条件的所有依赖项版本,然后将它们写入 Cargo.lock
文件。当未来再次构建项目时,Cargo
将查阅 Cargo.lock
文件存在并使用其中指定的版本,而不是再次执行所有确定版本的工作。这使开发者可以自动进行可重现的构建。换句话说,由于Cargo.lock
文件,您的项目将一直保持使用 0.8.5
的 rand
函数,直到你明确需要升级更新。
附2. 更新 crate 版本
当你确实想更新一个 crate
时,Cargo
会提供升级命令 cargo update
,它会忽略 Cargo.lock
文件并在 Cargo.toml
中找出符合你规格的所有最新版本。Cargo
会将这些版本写入 Cargo.lock
文件。否则,默认情况下,Cargo
只会查找大于 0.8.5
和小于的版本 0.9.0
。如果 randcrate
已经发布了两个新版本 0.8.6
和 0.9.0
,这个时候如果运行 cargo update
,将看到以下内容:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo
会忽略 0.9.0
版本。同时,你会发现 Cargo.lock
文件也被修改,将 rand
的版本改为最新的 rand crate 0.8.6
。如果需要使用到 0.9.0
或 0.9.x
系列版本,那么必须手动的更新 Cargo.toml
文件。
[dependencies]
rand = "0.9.0"
修改完后,当下次运行 cargo build
时,Cargo
将会更新可用的 crate
注册表,并重新下载编译你需要的新版本。
Boys and Girls!!!
准备好了吗?下一节我们要开始做个小练习了哦!
不!我还没准备好,让我先回顾一下之前的。
上一篇《使用 Cargo 构建管理 Rust 项目——Rust语言基础03》
我准备好了,掛かって来い(放马过来)!
下一篇《Rust 中的变量(特性)——Rust语言基础05》
觉得这篇文章对你有帮助的话,就留下一个赞吧v*
请尊重作者,转载还请注明出处!感谢配合~
[作者]: Imagine Miracle
[版权]: 本作品采用知识共享署名-非商业性-相同方式共享 4.0 国际许可协议进行许可。
[本文链接]: https://blog.csdn.net/qq_36393978/article/details/125208224