对猜谜游戏进行编程
让我们一起完成一个动手项目,进入 Rust!本章通过向您展示如何在实际程序中使用它们,向您介绍一些常见的 Rust 概念。您将了解 let
、match
、方法、关联函数、外部 crate 等!在接下来的章节中,我们将更详细地探讨这些概念。在本章中,您将只练习基础知识。
我们将实现一个经典的初学者编程问题:猜谜游戏。它是这样工作的:程序将生成一个介于 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.org/cargo/reference/manifest.html
[dependencies]
正如你在第 1 章中看到的,cargo new
为你生成了一个 “Hello, world!” 程序。查看 src/main.rs 文件:
文件名: src/main.rs
fn main() { println!("Hello, world!"); }
现在让我们编译这个 “Hello, world!” 程序,并使用 cargo run
命令以相同的步骤运行它:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
当您需要快速迭代项目时,run
命令会派上用场,就像我们在这个游戏中所做的那样,在继续下一个迭代之前快速测试每个迭代。
重新打开 src/main.rs 文件。您将在此文件中编写所有代码。
处理猜测
猜谜游戏程序的第一部分会要求用户输入,处理
该 input,并检查输入是否为预期形式。首先,我们将
允许玩家输入猜测。将示例 2-1 中的代码输入到
src/main.rs 中。
文件名: 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);
}
示例 2-1:从用户那里获取猜测并打印它的代码
这段代码包含了很多信息,所以让我们一行一行地看一遍。自
获取用户输入,然后将结果打印为输出,我们需要将
IO
input/output 库设置为 Scope。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
语句创建变量。这是另一个示例:
设 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
函数,因为它是生成某种新值的函数的通用名称。
该 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);
}
如果我们没有使用 std::io;
导入 io
库,请在
程序中,我们仍然可以通过将这个函数调用编写为
std::io::stdin
的stdin
函数返回
std::io::Stdin
,这是一个表示终端标准输入句柄的类型。
接下来,行 .read_line(&mut guess)
调用标准输入句柄上的 read_line
方法以获取用户的输入。我们还将 &mut guess
作为参数传递给 read_line
以告诉它要将用户输入存储在哪个字符串中。read_line
的全部工作是将用户输入的任何内容放入标准输入中,并将其附加到字符串中(不覆盖其内容),因此我们将该字符串作为参数传递。string 参数需要是可变的,以便方法可以更改字符串的内容。
&
表示此参数是一个引用,它为您提供了一种方法,可以让代码的多个部分访问一个数据,而无需多次将该数据复制到内存中。引用是一个复杂的功能,Rust 的主要优势之一是使用引用是多么安全和容易。您无需了解很多细节即可完成此程序。现在,您需要知道的是,与变量一样,引用默认是不可变的。因此,您需要编写 &mut guess
而不是
&guess
使其可变。(第 4 章将更详细地解释参考文献。
处理潜在故障的结果
我们仍在处理这行代码。我们现在正在讨论第三行文本,但请注意,它仍然是单个逻辑代码行的一部分。下一部分是这种方法:
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
是一个枚举,通常称为枚举,它是一种可以处于多种可能状态之一的类型。我们将每个可能的状态称为 variant。
第 6 章将更详细地介绍枚举。这些 Result
类型的目的是对错误处理信息进行编码。
Result
的变体为 Ok
和 Err
。的 Ok
变体表示作成功,Ok
内部是成功生成的值。Err
变体表示作失败,而 Err
包含有关作如何或为何失败的信息。
Result
类型的值与任何类型的值一样,都定义了方法。Result
的实例具有 expect
方法
你可以打电话。如果 Result
的此实例是 Err
值,则期望
将导致程序崩溃并显示您作为
argument 来期望
。如果 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` profile [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 和 y + 2 = 12
。
测试 First Part
让我们测试猜谜游戏的第一部分。使用 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,它是一个可执行文件。兰特
crate 是一个库 crate,它包含旨在在其他程序中使用且不能单独执行的代码。
Cargo 对外部板条箱的协调是 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 认为这些版本具有与 0.8.5 版本兼容的公共 API,此规范可确保您获得最新的补丁版本,该版本仍将使用本章中的代码进行编译。不保证任何版本 0.9.0 或更高版本的 API 与以下示例使用的 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
示例 2-2:将 rand crate 添加为依赖项后运行
cargo build
的输出
你可能会看到不同的版本号(但多亏了 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 中的任何版本。倍
系列,则必须更新 Cargo.toml 文件,使其如下所示:
[dependencies]
rand = "0.9.0"
下次运行 cargo build
时,Cargo 将更新可用 crate 的注册表,并根据您指定的新版本重新评估您的 rand
要求。
关于 Cargo 及其
ecosystem,我们将在第 14 章中讨论,但现在,这就是你需要知道的全部内容。Cargo 使重用库变得非常容易,因此 Rustacean 能够编写由许多包组装而成的较小项目。
生成随机数
让我们开始使用 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}");
}
示例 2-3:添加代码以生成随机数
首先,我们添加 use rand::Rng;
行。Rng
trait 定义了随机数生成器实现的方法,并且这个 trait 必须在范围内,我们才能使用这些方法。第 10 章将详细介绍特征。
接下来,我们在中间添加两行。在第一行中,我们调用
rand::thread_rng
函数,它为我们提供了要使用的特定随机数生成器:一个位于当前执行线程本地并由作系统播种的随机数生成器。然后我们调用 gen_range
方法。此方法由 Rng
定义
trait 中引入的 trait
中。这
gen_range
方法将范围表达式作为参数,并在范围内生成一个随机数。我们在这里使用的范围表达式类型为 start..=end
的 URL 中,并且包含下限和上限,因此我们需要指定 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!"),
}
}
示例 2-4:处理比较两个数字的可能返回值
首先,我们添加另一个 use
语句,引入一个名为
std::cmp::Ordering
into scope 从标准库中。Ordering
类型是另一个枚举,具有变体 Less
、Greater
和 Equal
。这是比较两个值时可能出现的三种结果。
然后,我们在底部添加 5 行使用 Ordering
类型的新行。这
cmp
方法比较两个值,并且可以对任何可以比较的值调用。它引用了你想要比较的任何内容:这里它将 guess
与 secret_number
进行比较。然后,它返回
Ordering
enum 我们使用 use
语句引入范围。我们使用
match
表达式,根据调用 cmp
返回的 Ordering
变体(其值为 guess
和 secret_number
)来决定下一步要做什么。
match
表达式由 arms 组成。arm 由要匹配的模式和在给定的值为 match
时应运行的代码组成
适合那只手臂的模式。Rust 采用给定的 match
值,并依次查看每个 arm 的模式。模式和 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
!该 arm 中的关联代码将执行并在屏幕上打印 Too big!
。比赛
表达式在第一次成功匹配后结束,因此它不会查看最后一个
ARM 的 S 实例。
但是,示例 2-4 中的代码还不能编译。让我们试一试:
$ cargo build
Downloading crates ...
Downloaded rand_core v0.6.2
Downloaded getrandom v0.2.2
Downloaded rand_chacha v0.3.0
Downloaded ppv-lite86 v0.2.10
Downloaded libc v0.2.86
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/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/cmp.rs:839: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
转换为数字类型,以便我们可以将其与秘密数字进行数值比较。我们通过将这行添加到主
函数主体中来实现:
文件名: 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 允许我们将 guess
的先前值与新的值相遮蔽。Shadowing 让我们可以重用猜测
变量名称,而不是强制我们创建两个唯一的变量,例如
例如,guess_str
和 guess
。我们将在
第 3 章,但现在要知道,当您想将值从一种类型转换为另一种类型时,通常会使用此功能。
我们将这个新变量绑定到表达式 guess.trim().parse()。
猜测
引用包含输入作为字符串的原始 guess
变量。String
实例上的 trim
方法将消除开头和结尾的任何空格,我们必须这样做才能将字符串与 u32 进行比较,u32
只能包含数值数据。用户必须按
enter 来满足read_line
并输入他们的猜测,这会向字符串中添加一个换行符。例如,如果用户键入 5 并按 enter ,guess
将如下所示:5\n
。\n
表示 “newline”。(在 Windows 上,按 enter 将产生回车和换行符 \r\n
。trim
方法消除了 \n
或 \r\n
,因此只有 5
。
字符串的 parse
方法将字符串转换为另一种类型。在这里,我们使用它从字符串转换为数字。我们需要使用 let guess 告诉 Rust 我们想要的确切数字类型:u32
。guess
后面的冒号 (:
) 告诉 Rust 我们将注释变量的类型。Rust 有一些内置的数字类型;此处看到的 U32
是一个无符号的 32 位整数。对于较小的正数,这是一个不错的默认选择。您将在第 3 章中了解其他数字类型。
此外,此示例程序中的 u32
注解以及与 secret_number
的比较意味着 Rust 将推断 secret_number
应该是
U32
也是如此。所以现在比较将是相同类型的两个值之间的比较!
parse
方法仅适用于逻辑上可以转换为数字的字符,因此很容易导致错误。例如,如果字符串包含 A👍%,
则无法将其转换为数字。因为它可能会失败,所以 parse
方法返回 Result
类型,就像 read_line
方法执行
结果
”)。我们将再次使用 expect
方法以相同的方式处理此 Result
。如果解析
返回一个 Err
Result
变体,因为它无法从字符串中创建数字,则 expect
调用将使游戏崩溃并打印我们给它的消息。如果 parse
可以成功地将字符串转换为数字,它将返回
Ok
变体的
Result 和 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!"),
}
}
}
如你所见,我们已将 guess input 提示中的所有内容移动到一个循环中。确保将循环内的行再缩进 4 个空格,然后再次运行程序。该程序现在将永远要求再次猜测,这实际上引入了一个新问题。用户似乎无法退出!
用户始终可以使用键盘快捷键中断程序
ctrl - c .但是还有另一种方法可以逃脱这个贪得无厌的怪物,正如 “将猜测与
Secret Number“:如果用户输入非数字答案,程序将崩溃。我们可以利用这一点来允许用户退出,如下所示:
$ 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!
之后添加换行
符会使程序在用户正确猜出秘密数字时退出循环。退出 loop 也意味着退出程序,因为 loop 是 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;
}
}
}
}
示例 2-5:忽略非数字猜测并要求再次猜测而不是使程序崩溃
我们从 expect
调用切换到 match
表达式,从错误崩溃转变为处理错误。请记住,parse
会返回一个 Result
type 和 Result
是具有变体 Ok
和 Err
的枚举。我们在这里使用了 match
表达式,就像我们对 cmp
的 Ordering
结果所做的那样
方法。
如果 parse
能够成功地将字符串转换为数字,它将返回一个包含结果数字的 Ok
值。该 Ok
值将匹配第一个分支的模式,并且 match
表达式将只返回
num
值,该值
解析生成并放入 Ok
值中。该数字将出现在我们正在创建的新 guess
变量中所需的位置。
如果 parse
无法将字符串转换为数字,它将返回一个
Err
值,其中包含有关错误的更多信息。Err
值与第一个匹配
臂中的 Ok(num)
模式不匹配,但它与第二个匹配臂中的 Err(_)
模式匹配。下划线 _
是 catchall 值;在此示例中,我们表示要匹配所有 Err
值,无论它们里面有什么信息。所以程序将
执行第二个 Arm 的代码 continue
,该代码告诉程序转到循环
的下一次迭代并请求另一次猜测。因此,实际上,该程序会忽略 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!
棒!通过最后的一个小调整,我们将完成猜谜游戏。回想一下,程序仍在打印密钥号码。这在测试中效果很好,但它会毁了游戏。让我们删除输出 secret 数字的 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;
}
}
}
}
示例 2-6:完成猜谜游戏代码
此时,您已经成功构建了猜谜游戏。祝贺!
总结
这个项目是向你介绍许多新 Rust 概念的实践方式:
let
、match
、函数、外部 crate 的使用等等。在接下来的几章中,您将更详细地了解这些概念。第 3 章涵盖了大多数编程语言都有的概念,例如变量、数据类型和函数,并展示了如何在 Rust 中使用它们。第 4 章探讨了所有权,这是 Rust 不同于其他语言的一个特性。第 5 章讨论了结构和方法语法,第 6 章解释了枚举是如何工作的。