编写猜谜游戏
让我们通过一起完成一个实践项目来学习 Rust!本章将通过展示如何在实际程序中使用一些常见的 Rust 概念来向您介绍它们。您将学习 let
、match
、方法、关联函数、外部箱等等!在接下来的章节中,我们将更详细地探讨这些概念。在本章中,您将只练习基础知识。
我们将实现一个经典的编程入门问题:猜谜游戏。以下是它的工作原理:程序将生成一个介于 1 到 100 之间的随机整数。然后它将提示玩家输入一个猜测。在输入猜测后,程序将指示猜测是太低还是太高。如果猜测正确,游戏将打印祝贺消息并退出。
设置新项目
要设置新项目,请转到您在第 1 章中创建的 *projects* 目录,并使用 Cargo 创建一个新项目,如下所示
$ cargo new guessing_game
$ cd guessing_game
第一个命令 cargo new
将项目名称 (guessing_game
) 作为第一个参数。第二个命令切换到新项目的目录。
查看生成的 *Cargo.toml* 文件
文件名:Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.net.cn/cargo/reference/manifest.html
[dependencies]
正如您在第 1 章中所见,cargo new
会为您生成一个“你好,世界!”程序。查看 *src/main.rs* 文件
文件名:src/main.rs
fn main() { println!("Hello, world!"); }
现在让我们编译这个“你好,世界!”程序,并使用 cargo run
命令一步运行它
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
当您需要快速迭代项目时,run
命令会派上用场,就像我们在这个游戏中所做的那样,在进入下一个迭代之前快速测试每个迭代。
重新打开 src/main.rs 文件。您将在该文件中编写所有代码。
处理猜测
猜谜游戏程序的第一部分将要求用户输入,处理该输入,并检查输入是否符合预期格式。首先,我们将允许玩家输入猜测。在 src/main.rs 中输入代码清单 2-1 中的代码。
文件名:src/main.rs
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
。
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);
}
默认情况下,Rust 在标准库中定义了一组项目,这些项目会被引入到每个程序的作用域中。这组项目称为 *prelude*,您可以在标准库文档中查看所有项目。
如果您要使用的类型不在 prelude 中,则必须使用 use
语句显式地将其引入作用域。使用 std::io
库为您提供了许多有用的功能,包括接受用户输入的功能。
正如您在第 1 章中所见,main
函数是程序的入口点。
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);
}
fn
语法声明了一个新函数;圆括号 ()
表示没有参数;花括号 {
表示函数体的开始。
正如您在第 1 章中所学到的,println!
是一个将字符串打印到屏幕上的宏。
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);
}
这段代码打印了一个提示,说明了游戏的内容并请求用户输入。
使用变量存储值
接下来,我们将创建一个 *变量* 来存储用户输入,如下所示:
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);
}
现在程序变得有趣了!这短短的一行代码中发生了很多事情。我们使用 let
语句来创建变量。下面是另一个例子:
let apples = 5;
这行代码创建了一个名为 apples
的新变量,并将其绑定到值 5。在 Rust 中,变量默认是不可变的,这意味着一旦我们给变量赋了一个值,该值就不会改变。我们将在第 3 章的“变量和可变性”部分详细讨论这个概念。要使变量可变,我们在变量名前面添加 mut
:
let apples = 5; // immutable
let mut bananas = 5; // mutable
注意:
//
语法开始一个注释,该注释一直持续到行尾。Rust 会忽略注释中的所有内容。我们将在第 3 章中更详细地讨论注释。.
回到猜谜游戏程序,您现在知道 let mut guess
将引入一个名为 guess
的可变变量。等号 (=
) 告诉 Rust 我们现在想将一些东西绑定到该变量。等号右边是 guess
绑定的值,它是调用 String::new
的结果,该函数返回一个 String
的新实例。String
是由标准库提供的一种字符串类型,它是一个可增长的、UTF-8 编码的文本位。
::new
行中的 ::
语法表示 new
是 String
类型的一个关联函数。*关联函数* 是在一个类型上实现的函数,在本例中是 String
。这个 new
函数创建一个新的空字符串。您会在许多类型上找到 new
函数,因为它是创建某种类型的新值的函数的常用名称。
总而言之,let mut guess = String::new();
行创建了一个可变变量,该变量当前绑定到一个 String
的新的空实例。呼!
接收用户输入
回想一下,我们在程序的第一行使用 use std::io;
包含了标准库中的输入/输出功能。现在,我们将调用 io
模块中的 stdin
函数,该函数将允许我们处理用户输入:
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);
}
如果我们没有在程序开头使用 use std::io;
导入 io
库,我们仍然可以通过将此函数调用写成 std::io::stdin
来使用该函数。stdin
函数返回一个 std::io::Stdin
的实例,该类型表示终端标准输入的句柄。
接下来,.read_line(&mut guess)
行调用标准输入句柄上的 read_line
方法来获取用户输入。我们还将 &mut guess
作为参数传递给 read_line
,以告诉它将用户输入存储在哪个字符串中。read_line
的全部工作是获取用户在标准输入中键入的任何内容,并将其追加到一个字符串中(不覆盖其内容),因此我们将该字符串作为参数传递。字符串参数必须是可变的,以便该方法可以更改字符串的内容。
&
表示此参数是一个 *引用*,它为您提供了一种方法,可以让代码的多个部分访问一段数据,而无需多次将该数据复制到内存中。引用是一个复杂的功能,而 Rust 的一个主要优势是使用引用是多么安全和容易。您不需要了解很多细节就可以完成这个程序。现在,您只需要知道,像变量一样,引用默认是不可变的。因此,您需要编写 &mut guess
而不是 &guess
来使其可变。(第 4 章将更详细地解释引用。)
使用 Result
处理潜在的失败
我们仍在处理这行代码。我们现在正在讨论第三行文本,但请注意,它仍然是一行逻辑代码的一部分。下一部分是这个方法:
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::stdin().read_line(&mut guess).expect("Failed to read line");
然而,一行太长难以阅读,所以最好将其分开。当您使用 .method_name()
语法调用方法时,通常明智的做法是引入换行符和其他空格来帮助断开长行。现在让我们讨论一下这行代码的作用。
如前所述,read_line
将用户输入的任何内容放入我们传递给它的字符串中,但它也会返回一个 Result
值。Result
是一个 *枚举*,通常称为 *enum*,它是一种可以处于多种可能状态之一的类型。我们将每个可能的状态称为一个 *变体*。
第 6 章将更详细地介绍枚举。这些 Result
类型的目的是编码错误处理信息。
Result
的变体是 Ok
和 Err
。Ok
变体表示操作成功,Ok
内部是成功生成的值。Err
变体表示操作失败,Err
包含有关操作如何或为何失败的信息。
Result
类型的值,像任何类型的值一样,都有在其上定义的方法。Result
的实例有一个expect
方法,您可以调用它。如果这个 Result
的实例是一个 Err
值,expect
将导致程序崩溃并显示您作为参数传递给 expect
的消息。如果 read_line
方法返回一个 Err
,它很可能是底层操作系统出现错误的结果。如果这个 Result
的实例是一个 Ok
值,expect
将获取 Ok
持有的返回值,并将其返回给您,以便您可以使用它。在本例中,该值是用户输入的字节数。
如果您不调用 expect
,程序将编译,但您会收到一个警告:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().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().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust 警告您没有使用 read_line
返回的 Result
值,这表明程序没有处理可能的错误。
抑制警告的正确方法是实际编写错误处理代码,但在我们的例子中,我们只想在出现问题时使程序崩溃,因此我们可以使用 expect
。您将在第 9 章中学习如何从错误中恢复。.
使用 println!
占位符打印值
除了右花括号之外,到目前为止,代码中只有一行需要讨论:
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);
}
这行代码打印现在包含用户输入的字符串。{}
这组花括号是一个占位符:将 {}
想象成一对小螃蟹钳,将一个值固定在适当的位置。当打印变量的值时,变量名可以放在花括号内。当打印表达式求值的结果时,在格式字符串中放置空的花括号,然后在格式字符串后面跟一个逗号分隔的表达式列表,以按照相同的顺序打印在每个空的花括号占位符中。在一次调用 println!
中打印一个变量和一个表达式的结果如下所示:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
这段代码将打印 x = 5 and y + 2 = 12
。
测试第一部分
让我们测试猜谜游戏的第一部分。使用 cargo run
运行它:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
至此,游戏的第一部分就完成了:我们从键盘获取输入,然后打印出来。
生成一个秘密数字
接下来,我们需要生成一个用户将尝试猜测的秘密数字。每次的秘密数字都应该不同,这样游戏玩起来才会有趣。我们将使用 1 到 100 之间的随机数,这样游戏就不会太难。Rust 的标准库中还没有包含随机数功能。但是,Rust 团队确实提供了一个具有该功能的rand
crate。
使用 Crate 获取更多功能
请记住,crate 是 Rust 源代码文件的集合。我们一直在构建的项目是一个 *二进制 crate*,它是一个可执行文件。rand
crate 是一个 *库 crate*,它包含旨在在其他程序中使用的代码,并且不能单独执行。
Cargo 对外部 crate 的协调是 Cargo 真正闪光的地方。在我们编写使用 rand
的代码之前,我们需要修改 Cargo.toml 文件,将 rand
crate 作为依赖项包含进来。现在打开该文件,并在 Cargo 为您创建的 [dependencies]
部分标题下方添加以下行。请确保完全按照我们在这里的方式指定 rand
,并使用此版本号,否则本教程中的代码示例可能无法正常工作:
文件名:Cargo.toml
[dependencies]
rand = "0.8.5"
在 Cargo.toml 文件中,标题后面的所有内容都是该部分的一部分,该部分一直持续到下一个部分开始。在 [dependencies]
中,您可以告诉 Cargo 您的项目依赖于哪些外部 crate,以及您需要这些 crate 的哪些版本。在本例中,我们使用语义版本说明符 0.8.5
指定了 rand
crate。Cargo 理解语义版本控制。(有时称为 SemVer),它是编写版本号的标准。说明符 0.8.5
实际上是 ^0.8.5
的简写,表示任何至少为 0.8.5 但低于 0.9.0 的版本。
Cargo 认为这些版本的公共 API 与版本 0.8.5 兼容,并且此规范确保您将获得最新的补丁版本,该版本仍可与本章中的代码一起编译。任何 0.9.0 或更高版本都不能保证具有与以下示例相同的 API。
现在,在不更改任何代码的情况下,让我们构建项目,如清单 2-2 所示。
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
您可能会看到不同的版本号(但由于 SemVer,它们都将与代码兼容!)和不同的行(取决于操作系统),并且这些行的顺序可能不同。
当我们包含外部依赖项时,Cargo 会从*注册表*中获取该依赖项所需的所有内容的最新版本,该注册表是 Crates.io 数据的副本。Crates.io 是 Rust 生态系统中的人们发布他们的开源 Rust 项目供他人使用的地方。
更新注册表后,Cargo 会检查 [dependencies]
部分并下载尚未下载的任何列出的 crate。在这种情况下,虽然我们只列出了 rand
作为依赖项,但 Cargo 还获取了 rand
依赖于工作的其他 crate。下载 crate 后,Rust 会编译它们,然后使用可用的依赖项编译项目。
如果您在不做任何更改的情况下立即再次运行 cargo build
,您将不会获得除 Finished
行之外的任何输出。Cargo 知道它已经下载并编译了依赖项,并且您没有在 Cargo.toml 文件中更改任何内容。Cargo 还知道您没有更改任何代码,因此它也不会重新编译。由于无事可做,它只是退出。
如果您打开 src/main.rs 文件,进行一些微不足道的更改,然后保存并再次构建,您将只看到两行输出
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
这些行表明 Cargo 仅使用您对 src/main.rs 文件的微小更改来更新构建。您的依赖项没有改变,因此 Cargo 知道它可以重用它已经为这些依赖项下载和编译的内容。
使用 Cargo.lock 文件确保可重现的构建
Cargo 有一种机制可以确保您或任何其他人每次构建代码时都可以重建相同的工件:Cargo 将仅使用您指定的依赖项版本,直到您另行指示。例如,假设下周发布了 rand
crate 的 0.8.6 版本,并且该版本包含一个重要的错误修复,但它也包含一个会破坏您的代码的回归。为了解决这个问题,Rust 在您第一次运行 cargo build
时创建了 Cargo.lock 文件,所以我们现在在 guessing_game 目录中有了这个文件。
当您第一次构建项目时,Cargo 会找出符合条件的所有依赖项版本,然后将它们写入 Cargo.lock 文件。将来构建项目时,Cargo 会看到 Cargo.lock 文件存在,并将使用其中指定的版本,而不是再次完成确定版本的所有工作。这使您可以自动进行可重现的构建。换句话说,由于 Cargo.lock 文件,您的项目将保持在 0.8.5 版本,直到您明确升级。因为 Cargo.lock 文件对于可重现的构建很重要,所以它经常与项目中的其余代码一起检入源代码控制。
更新 Crate 以获取新版本
当您*确实*想要更新 crate 时,Cargo 提供了命令 update
,它将忽略 Cargo.lock 文件并找出符合 Cargo.toml 中规范的所有最新版本。然后 Cargo 会将这些版本写入 Cargo.lock 文件。在这种情况下,Cargo 只会查找大于 0.8.5 且小于 0.9.0 的版本。如果 rand
crate 发布了两个新版本 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
crate 版本为 0.8.6。要使用 rand
版本 0.9.0 或 0.9.x 系列中的任何版本,您必须更新 Cargo.toml 文件,使其如下所示
[dependencies]
rand = "0.9.0"
下次运行 cargo build
时,Cargo 将更新可用的 crate 注册表,并根据您指定的新版本重新评估您的 rand
要求。
关于 Cargo和 它的生态系统还有很多要说的,我们将在第 14 章讨论,但就目前而言,这就是您需要知道的全部内容。Cargo 使得重用库变得非常容易,因此 Rustaceans 能够编写由多个包组装而成的小型项目。
生成随机数
让我们开始使用 rand
生成一个要猜测的数字。下一步是更新 src/main.rs,如清单 2-3 所示。
文件名:src/main.rs
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 定义了随机数生成器实现的方法,并且此 trait 必须在范围内才能让我们使用这些方法。第 10 章将详细介绍 trait。
接下来,我们在中间添加两行。在第一行中,我们调用 rand::thread_rng
函数,该函数为我们提供了我们将要使用的特定随机数生成器:一个特定于当前执行线程并由操作系统播种的生成器。然后我们在随机数生成器上调用 gen_range
方法。此方法由我们使用 use rand::Rng;
语句引入范围的 Rng
trait 定义。gen_range
方法将范围表达式作为参数,并在该范围内生成一个随机数。我们在这里使用的范围表达式采用 start..=end
的形式,并且包含上下限,因此我们需要指定 1..=100
来请求 1 到 100 之间的数字。
注意:您不会仅仅知道要使用哪些 trait 以及要从 crate 中调用哪些方法和函数,因此每个 crate 都有说明如何使用它的文档。Cargo 的另一个简洁功能是运行
cargo doc --open
命令将在本地构建所有依赖项提供的文档并在您的浏览器中打开它。例如,如果您对rand
crate 中的其他功能感兴趣,请运行cargo doc --open
并单击左侧边栏中的rand
。
第二行新代码打印了秘密数字。这在我们开发程序以进行测试时很有用,但我们将在最终版本中将其删除。如果程序一开始就打印出答案,那就不是什么游戏了!
尝试多次运行程序
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
您应该会得到不同的随机数,并且它们都应该是 1 到 100 之间的数字。干得好!
将猜测与秘密数字进行比较
现在我们有了用户输入和一个随机数,我们可以比较它们。该步骤如清单 2-4 所示。请注意,此代码尚无法编译,我们将在后面解释。
文件名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
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
类型是另一个枚举,具有变体 Less
、Greater
和 Equal
。这些是比较两个值时可能出现的三个结果。
然后我们在底部添加了五行使用 Ordering
类型的新代码。cmp
方法比较两个值,并且可以调用任何可以比较的东西。它接受对您要与之比较的任何内容的引用:这里它正在将 guess
与 secret_number
进行比较。然后它返回我们使用 use
语句引入范围的 Ordering
枚举的变体。我们使用 match
表达式根据使用 guess
和 secret_number
中的值调用 cmp
返回的 Ordering
变体来决定下一步做什么。
match
表达式由多个 分支(arm) 构成。每个分支都包含一个用于匹配的 模式(pattern),以及当 match
接收到的值符合该分支的模式时要运行的代码。Rust 会获取传递给 match
的值,并依次检查每个分支的模式。模式和 match
结构是 Rust 的强大功能:它们允许您表达代码可能遇到的各种情况,并确保您能够处理所有情况。这些功能将分别在第 6 章和第 18 章中详细介绍。
让我们通过一个示例来逐步了解我们在这里使用的 match
表达式。假设用户猜了 50,而这次随机生成的秘密数字是 38。
当代码将 50 与 38 进行比较时,cmp
方法将返回 Ordering::Greater
,因为 50 大于 38。match
表达式获取 Ordering::Greater
值,并开始检查每个分支的模式。它查看第一个分支的模式 Ordering::Less
,发现值 Ordering::Greater
与 Ordering::Less
不匹配,因此它会忽略该分支中的代码,并移至下一个分支。下一个分支的模式是 Ordering::Greater
,它 确实 与 Ordering::Greater
匹配!该分支中的关联代码将执行,并在屏幕上打印 Too big!
。match
表达式在第一次成功匹配后结束,因此在这种情况下,它不会查看最后一个分支。
但是,代码清单 2-4 中的代码还不能编译。让我们试一试
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/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
--> /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/cmp.rs:814:8
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
类型,而无需我们显式地编写类型。另一方面,secret_number
是一个数字类型。Rust 中有几种数字类型可以表示 1 到 100 之间的值:i32
,一个 32 位整数;u32
,一个无符号 32 位整数;i64
,一个 64 位整数;以及其他类型。除非另有指定,否则 Rust 默认使用 i32
,它是 secret_number
的类型,除非您在其他地方添加了类型信息,导致 Rust 推断出不同的数字类型。出现错误的原因是 Rust 无法比较字符串和数字类型。
最终,我们希望将程序读取为输入的 String
转换为数字类型,以便我们可以将其与秘密数字进行数值比较。我们通过在 main
函数体中添加以下行来实现这一点
文件名:src/main.rs
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.");
// --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!"),
}
}
该行是
let guess: u32 = guess.trim().parse().expect("Please type a number!");
我们创建了一个名为 guess
的变量。但是等等,程序中不是已经有一个名为 guess
的变量了吗?是的,但幸运的是,Rust 允许我们使用新值来 遮蔽(shadow) 之前 guess
的值。遮蔽允许我们重用 guess
变量名,而不必强制我们创建两个唯一的变量,例如 guess_str
和 guess
。我们将在 第 3 章 中更详细地介绍这一点,但现在,您只需要知道,当您想将值从一种类型转换为另一种类型时,经常会使用此功能。
我们将这个新变量绑定到表达式 guess.trim().parse()
。表达式中的 guess
指的是原始的 guess
变量,它包含作为字符串的输入。String
实例上的 trim
方法将消除开头和结尾的任何空格,我们必须这样做才能将字符串与 u32
进行比较,后者只能包含数字数据。用户必须按 Enter 键才能满足 read_line
并输入他们的猜测,这会在字符串中添加一个换行符。例如,如果用户键入 5 并按 Enter 键,则 guess
看起来像这样:5\n
。\n
表示“换行”。(在 Windows 上,按 Enter 键会导致回车和换行,即 \r\n
。)trim
方法会消除 \n
或 \r\n
,最终只留下 5
。
字符串上的 parse
方法将字符串转换为另一种类型。在这里,我们使用它将字符串转换为数字。我们需要使用 let guess: u32
来告诉 Rust 我们想要的具体数字类型。guess
后的冒号 (:
) 告诉 Rust 我们将注释变量的类型。Rust 有一些内置的数字类型;这里看到的 u32
是一个无符号的 32 位整数。对于小的正数来说,这是一个不错的默认选择。您将在 第 3 章 中了解其他数字类型.
此外,本示例程序中的 u32
注释以及与 secret_number
的比较意味着 Rust 将推断出 secret_number
也应该是 u32
类型。所以现在比较将在相同类型的两个值之间进行!
parse
方法只能处理可以逻辑转换为数字的字符,因此很容易导致错误。例如,如果字符串包含 A👍%
,则无法将其转换为数字。因为它可能会失败,所以 parse
方法返回一个 Result
类型,就像 read_line
方法一样(在前面的 “使用 Result 处理潜在的失败” 中讨论过)。我们将以相同的方式处理这个 Result
,再次使用 expect
方法。如果 parse
因为无法从字符串创建数字而返回一个 Err
Result
变体,则 expect
调用将使游戏崩溃,并打印我们给它的消息。如果 parse
可以成功地将字符串转换为数字,它将返回 Result
的 Ok
变体,并且 expect
将从 Ok
值中返回我们想要的数字。
现在让我们运行程序
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
很好!即使在猜测之前添加了空格,程序仍然能够识别出用户猜的是 76。多次运行程序,验证不同输入的不同行为:正确猜测数字,猜测过高的数字,以及猜测过低的数字。
我们现在已经完成了游戏的大部分功能,但用户只能猜一次。让我们通过添加循环来改变这一点!
使用循环允许多次猜测
loop
关键字创建一个无限循环。我们将添加一个循环,让用户有更多机会猜测数字
文件名:src/main.rs
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);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your 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!("You win!"),
}
}
}
如您所见,我们已将从猜测输入提示开始的所有内容都移到了一个循环中。确保将循环内的每一行都缩进四个空格,然后再次运行程序。程序现在将永远要求用户再次猜测,这实际上引入了一个新问题。用户似乎无法退出!
用户始终可以使用键盘快捷键 Ctrl+C 来中断程序。但还有另一种方法可以摆脱这个贪婪的怪物,如 “将猜测与秘密数字进行比较” 中的 parse
讨论中所述:如果用户输入了非数字答案,程序将崩溃。我们可以利用这一点来允许用户退出,如下所示
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
键入 quit
将退出游戏,但您会注意到,输入任何其他非数字输入也会退出游戏。至少可以说,这不是最佳选择;我们希望游戏在猜对数字时也能停止。
在正确猜测后退出
让我们通过添加 break
语句来对游戏进行编程,使其在用户获胜时退出
文件名:src/main.rs
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}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
在 You win!
之后添加 break
行会使程序在用户正确猜测秘密数字时退出循环。退出循环也意味着退出程序,因为循环是 main
的最后一部分。
处理无效输入
为了进一步完善游戏的行为,当用户输入非数字时,不要使程序崩溃,而是让游戏忽略非数字,以便用户可以继续猜测。我们可以通过修改将 guess
从 String
转换为 u32
的那一行来实现这一点,如代码清单 2-5 所示。
文件名:src/main.rs
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();
// --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--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
我们从 expect
调用切换到 match
表达式,从错误崩溃转变为处理错误。请记住,parse
返回一个 Result
类型,而 Result
是一个具有 Ok
和 Err
变体的枚举。我们在这里使用 match
表达式,就像我们对 cmp
方法的 Ordering
结果所做的那样。
如果 parse
能够成功地将字符串转换为数字,它将返回一个包含结果数字的 Ok
值。该 Ok
值将与第一个分支的模式匹配,并且 match
表达式将只返回 parse
生成的并放在 Ok
值中的 num
值。该数字将最终出现在我们想要的位置,即我们正在创建的新 guess
变量中。
如果 parse
不能 将字符串转换为数字,它将返回一个 Err
值,其中包含有关错误的更多信息。Err
值与第一个 match
分支中的 Ok(num)
模式不匹配,但与第二个分支中的 Err(_)
模式匹配。下划线 _
是一个通配符值;在本例中,我们表示要匹配所有 Err
值,无论它们内部有什么信息。因此,程序将执行第二个分支的代码 continue
,它告诉程序进入 loop
的下一次迭代,并要求用户再次猜测。因此,实际上,程序会忽略 parse
可能遇到的所有错误!
现在,程序中的所有内容都应该按预期工作了。让我们试一试
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
太棒了!只需最后一个小小的调整,我们就可以完成猜谜游戏了。回想一下,程序仍在打印秘密数字。这对于测试来说很有效,但它破坏了游戏的乐趣。让我们删除输出秘密数字的 println!
。代码清单 2-6 显示了最终的代码。
文件名:src/main.rs
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;
}
}
}
}
至此,您已经成功地构建了猜谜游戏。恭喜!
总结
这个项目以一种动手实践的方式向您介绍了许多新的 Rust 概念:let
、match
、函数、外部 crate 的使用等等。在接下来的几章中,您将更详细地学习这些概念。第 3 章涵盖了大多数编程语言都具有的概念,例如变量、数据类型和函数,并展示了如何在 Rust 中使用它们。第 4 章探讨了所有权,这是 Rust 与其他语言不同的一个特性。第 5 章讨论了结构体和方法语法,第 6 章解释了枚举的工作原理。