上一篇:01-开始Rust之旅
本章通过演示如何在实际程序中使用 Rust,你将了解 let 、 match 、方法、关联函数、外部crate等基础知识。
本章将实现一个经典的初学者编程问题:猜谜游戏。
工作原理如下:程序将随机生成一个介于 1 和 100 之间的整数。然后,程序会提示玩家输入一个猜测。输入猜测值后,程序会显示猜测值是过低还是过高。如果猜测正确,游戏将打印一条祝贺信息并退出。
1. 创建一个新项目
使用 Cargo 创建一个新项目,如下所示:
$ cargo new guessing_game
$ cd guessing_game
第一条命令 cargo new 将项目名称 ( guessing_game ) 作为第一个参数。第二条命令会切换到新项目的目录。
查看生成的 Cargo.toml 文件:
[package]
name = "gussing_game"
version = "0.1.0"
edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
cargo new 会为您生成一个 "Hello, world!"程序。查看 src/main.rs 文件:
fn main() {println!("Hello, world!");
}
现在,让我们编译这个 "Hello, world!"程序,并使用 cargo run 命令在同一步骤中运行它:
$ cargo runCompiling gussing_game v0.1.0 (/home/username/rustProj/gussing_game)Finished dev [unoptimized + debuginfo] target(s) in 0.15sRunning `target/debug/gussing_game`
Hello, world!
当你需要快速迭代一个项目时, run 命令就会派上用场,就像我们在这个游戏中要做的那样,在进入下一个迭代之前快速测试每一次迭代。
重新打开 src/main.rs 文件。您将在此文件中编写所有代码。
2. 处理猜测
猜谜游戏程序的第一部分将要求用户输入信息,处理输入信息,并检查输入信息是否符合预期形式。首先,我们将允许玩家输入一个猜测。
use std::io;
fn main() {println!("Guess the number!");println!("Please input you guess:");let mut guess = String::new();io::stdin().read_line(&mut guess).expect("Failed to read line");println!("You guessed: {guess}");
}
这段代码包含大量信息,让我们逐行查看。要获取用户输入,然后将结果打印输出,我们需要将 io 输入/输出库纳入作用域。 io 库来自标准库,即 std ;
默认情况下,Rust 在标准库中定义了一组项目,并将其纳入每个程序的作用域。这组项目被称为前奏(prelude, /ˈpreljuːd/),你可以在标准库文档中看到其中的所有内容。
C++中的命名空间,Java中的包;
如果您想使用的类型不在前奏中,您必须使用 use 语句将该类型显式引入作用域。使用 std::io 库可以获得许多有用的功能,包括接受用户输入的能力。
2.1 用变量存储值
接下来,我们将创建一个变量来存储用户输入,就像这样:
let mut guess = String::new();
现在,节目开始变得有趣起来!在这一行中发生了很多事情。我们使用 let 语句创建变量。下面是另一个例子:
let apples = 5;
这一行创建了一个名为 apples 的新变量,并将其与值 5 绑定。在 Rust 中,变量默认是不可变的,这意味着一旦我们赋予变量一个值,这个值就不会改变。要使变量可变,我们可以在变量名前添加 mut :
let apples = 5; // immutable
let mut bananas = 5; // mutable
回到猜谜游戏程序,你现在知道 let mut guess 将引入一个名为 guess 的可变变量。等号 ( = ) 告诉 Rust 我们现在要绑定一个变量。等号的右边是 guess 绑定的值,它是调用 String::new 的结果,这个函数返回 String 的一个新实例。 String 是标准库提供的字符串类型,是一个可增长的、UTF-8 编码的文本位。
::new 行中的 :: 语法表明 new 是 String 类型的关联函数。关联函数是在一个类型(本例中为 String )上实现的函数。 new 函数创建一个新的空字符串。在许多类型中都可以找到 new 函数,因为它是创建某种新值的函数的通用名称。
关联函数,可以理解为C++上的类(静态)成员函数
总的来说, let mut guess = String::new(); 这一行创建了一个可变变量,该变量当前绑定到 String 的一个新的、空的实例。
2.2 接收用户输入
回想一下,我们在程序的第一行通过 use std::io; 包含了标准库中的输入/输出功能。现在,我们将调用 io 模块中的 stdin 函数,它将允许我们处理用户输入:
io::stdin().read_line(&mut guess)
如果我们没有在程序开始时用 use std::io; 导入 io 库,我们仍然可以通过将此函数调用写成 std::io::stdin 来使用该函数。 stdin 函数返回 std::io::Stdin 的一个实例,这是一种表示终端标准输入句柄的类型。
接下来, .read_line(&mut guess) 这一行在标准输入句柄上调用 read_line 方法来获取用户输入。我们还将 &mut guess 作为参数传递给 read_line ,告诉它将用户输入的内容存储在哪个字符串中。 read_line 的全部工作就是接收用户输入标准输入的内容,并将其追加到一个字符串中(不会覆盖其内容),因此我们要将该字符串作为参数传递给它。字符串参数必须是可变的,这样方法才能更改字符串的内容。
& 表示该参数是一个引用,这样就可以让代码的多个部分访问一段数据,而无需多次将该数据复制到内存中。因此,你需要编写 &mut guess 而不是 &guess 来使其可变。
2.3 处理Result潜在的异常
我们仍在研究这行代码。我们现在讨论的是第三行文字,但请注意,它仍然是单行逻辑代码的一部分。下一部分是这个方法:
.expect("Failed to read line");
我们可以将这段代码写成:
io::stdin().read_line(&mut guess).expect("Failed to read line");
不过,一长行字很难阅读,因此最好将其分割开来。在使用 .method_name() 语法调用方法时,引入换行符和其他空白来帮助分割长行通常是明智之举。现在我们来讨论一下这一行的作用。
如前所述, read_line 会将用户输入的任何内容放入我们传给它的字符串中,但它也会返回一个 Result 值。 Result 是一个枚举类型,是一种可以处于多种可能状态之一的类型。我们称每种可能的状态为一个变量。而Result的变量是:Ok和Err。Ok表示操作成功,Err表示操作失败, 包含操作失败的方式或原因的信息。
Result 类型的值与任何类型的值一样,都有为其定义的方法。 Result 的实例有一个可以调用的 expect 方法。如果 Result 的实例是 Err 值, expect 将导致程序崩溃,并显示作为参数传递给 expect 的信息。如果 read_line 方法返回的是 Err ,则很可能是底层操作系统出错所致。如果 Result 的实例是一个 Ok 值, expect 将获取 Ok 持有的返回值,并将该值返回给您,以便您可以使用它。在这种情况下,该值就是用户输入的字节数。
如果不调用 expect ,程序会编译成功,但会收到警告:
cargo.exe buildCompiling guessing_game v0.1.0 (E:\rustProj\guessing_game)
warning: unused `Result` that must be used--> src\main.rs:9:5|
9 | / io::stdin()
10 | | .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|
9 | let _ = io::stdin()| +++++++warning: `guessing_game` (bin "guessing_game") generated 1 warningFinished dev [unoptimized + debuginfo] target(s) in 0.33s
Rust 警告说,您还没有使用 read_line 返回的 Result 值,这表明程序还没有处理可能出现的错误。
抑制警告的正确方法是编写错误处理代码,但在我们的例子中,我们只想在出现问题时让程序崩溃,因此我们可以使用 expect 。
2.4 使用 println! 占位符打印数值
除了结尾的大括号,到目前为止,代码中只有一行需要讨论:
println!("You guessed: {guess}");
这一行打印的字符串现在包含用户输入的内容。 {} 中的一组大括号是占位符:把 {} 想象成一个小螃蟹钳子,用来固定数值。打印变量值时,变量名可以放在大括号内。打印表达式的运算结果时,在格式字符串中放置空的大括号,然后在格式字符串后以逗号分隔的表达式列表,按照相同的顺序打印到每个空的大括号占位符中。在对 println! 的一次调用中打印一个变量和一个表达式的结果将如下所示:
let x = 5;
let y = 10;println!("x = {x} and y + 2 = {}", y + 2);
这段代码将打印 :x = 5 and y + 2 = 12
2.5 测试上面代码
让我们来测试一下猜谜游戏的第一部分代码。使用 cargo run 运行:
cargo.exe runCompiling guessing_game v0.1.0 (E:\rustProj\guessing_game)Finished dev [unoptimized + debuginfo] target(s) in 0.33sRunning `target\debug\guessing_game.exe`
Guess the number!
Please input your guess:
8
Your guessed: 8
至此,游戏的第一部分已经完成:我们从键盘获取输入信息,然后打印出来。
3. 生成秘密号码
接下来,我们需要生成一个秘密数字,让用户来猜。秘密数字应该每次都不一样,这样游戏才会有趣,才能玩多次。我们将使用 1 到 100 之间的随机数,这样游戏就不会太难。Rust 的标准库中尚未包含随机数功能。不过,Rust 团队提供了一个具有上述功能的 rand crate。
3.1 使用crate以获得更多功能
crate 是 Rust 源代码文件的集合。我们正在构建的项目是一个二进制crate,也就是一个可执行文件。 rand crate是一个库板块,其中包含的代码用于其他程序,不能单独执行。
crate类似于C++中的Boost 库;
Cargo 对外部crate的协同是其真正的亮点所在。在编写使用 rand 的代码之前,我们需要修改 Cargo.toml 文件,将 rand crate 作为依赖项。
现在打开该文件,将下面一行添加到底部,即 Cargo 为你创建的 [dependencies] 部分标题的下方。请务必按照这里的版本号指定 rand ,否则本节中的代码示例可能无法运行:
[dependencies]
rand = "0.8.5"
在 Cargo.toml 文件中,头文件之后的所有内容都是该部分的一部分,一直持续到另一部分开始。在 [dependencies] 中,你可以告诉 Cargo 你的项目依赖于哪些外部crate,以及你需要这些crate的哪些版本。
在本例中,我们使用语义版本说明符 0.8.5 来指定 rand crate。Cargo能够理解语义版本(Semantic Versioning,有时也称为SemVer),这是一种编写版本号的标准。 0.8.5 实际上是 ^0.8.5 的缩写,意思是至少 0.8.5 但低于 0.9.0 的任何版本。
Cargo 认为这些版本具有与 0.8.5 版兼容的公共 API,而这一规范可确保您获得最新的补丁版本,并仍能与本章的代码编译。任何 0.9.0 或更高版本都不能保证与下面示例中使用的 API 相同。
现在,在不修改任何代码的情况下,让我们构建项目:
cargo.exe buildUpdating crates.io indexDownloaded rand_core v0.6.4Downloaded rand_chacha v0.3.1Downloaded ppv-lite86 v0.2.17Downloaded cfg-if v1.0.0Downloaded rand v0.8.5Downloaded getrandom v0.2.12Downloaded 6 crates (191.4 KB) in 1m 00sCompiling cfg-if v1.0.0Compiling ppv-lite86 v0.2.17Compiling getrandom v0.2.12Compiling rand_core v0.6.4Compiling rand_chacha v0.3.1Compiling rand v0.8.5Compiling guessing_game v0.1.0 (E:\rustProj\guessing_game)Finished dev [unoptimized + debuginfo] target(s) in 1m 03s
您可能会看到不同的版本号(但它们都与代码兼容,这要感谢 SemVer!)和不同的行数(取决于操作系统),而且行数的顺序也可能不同。
当我们加入一个外部依赖时,Cargo 会从注册表中获取该依赖所需的所有内容的最新版本,而注册表是来自 Crates.io 的数据副本。Crates.io 是 Rust 生态系统中的人们发布开源 Rust 项目供他人使用的地方。
更新注册表后,Cargo 会检查 [dependencies] 部分,并下载列出的任何尚未下载的crates。在本例中,虽然我们只将 rand 列为依赖关系,但 Cargo 还抓取了 rand 运行所依赖的其他 crates。下载完 crates 后,Rust 会对其进行编译,然后使用可用的依赖关系编译项目。
如果你不做任何修改就立即再次运行 cargo build ,除了 Finished 行之外,你不会得到任何输出。Cargo 知道它已经下载并编译了依赖项,而你也没有在 Cargo.toml 文件中对依赖项做任何修改。Cargo 也知道你没有修改代码,所以也不会重新编译。在无计可施的情况下,它会直接退出。
3.2 使用 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 文件对于可重现性构建非常重要,所以它通常会与项目中的其他代码一起进入源代码控制。
3.3 更新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.exe updateUpdating crates.io index
3.4 生成随机数
让我们开始使用 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..=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!("Your guessed: {guess}");}
首先,我们添加 use rand::Rng; 这一行。 Rng 特质定义了随机数生成器实现的方法,我们必须在该特质的作用域内才能使用这些方法。
接下来,我们在中间添加两行。在第一行,我们调用 rand::thread_rng 函数,该函数提供了我们要使用的特定随机数生成器:它是当前执行线程的本地生成器,由操作系统提供种子。然后,我们调用随机数发生器上的 gen_range 方法。该方法由 Rng 特性定义,我们通过 use rand::Rng; 语句将该特性引入作用域。 gen_range 方法以范围表达式为参数,生成范围内的随机数。我们在此使用的范围表达式形式为 start..=end ,并且包含下界和上界,因此我们需要指定 1..=100 来请求一个介于 1 和 100 之间的数字。
注意:你不可能只知道使用哪个特质、调用哪个方法和函数,因此每个crate都有使用说明文档。Cargo 的另一个特色是,运行 cargo doc --open 命令会在本地构建所有依赖项提供的文档,并在浏览器中打开。如果你对 rand crate 的其他功能感兴趣,运行 cargo doc --open 并点击左侧边栏中的 rand 即可。
cargo.exe doc --open Documenting cfg-if v1.0.0Documenting ppv-lite86 v0.2.17Documenting getrandom v0.2.12Documenting rand_core v0.6.4Documenting rand_chacha v0.3.1Documenting rand v0.8.5Documenting guessing_game v0.1.0 (D:\rustProj\guessing_game)Finished dev [unoptimized + debuginfo] target(s) in 4.36sOpening D:\rustProj\guessing_game\target\doc\guessing_game\index.html
第二行打印秘密号码。这在我们开发程序时很有用,可以用来测试程序,但我们会在最终版本中删除它。如果程序一开始就打印出答案,那就不算是游戏了!
3.4 将猜测与秘密数字进行比较
现在我们有了用户输入和随机数,可以对它们进行比较。该步骤如下代码所示。请注意,这段代码还不能编译,我们将对此进行说明。
use std::io;
use rand::Rng;
use std::cmp::Ordering;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!("Your 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 表达式,根据调用 cmp 所返回的 Ordering 变体与 guess 和 secret_number 中的值,决定下一步的操作。
match 表达式由arm组成。一个分支包括一个匹配模式,以及如果 match 中给出的值符合该分支的模式则应运行的代码。Rust 将输入 match 的值依次查看每个arm的模式。模式和 match 结构是 Rust 的强大功能:它们可以让你表达代码可能遇到的各种情况,并确保你能处理所有情况。
让我们用 match 表达式举个例子。假设用户猜中了 50,而这次随机生成的秘密数字是 38。
当代码比较 50 和 38 时, cmp 方法将返回 Ordering::Greater ,因为 50 大于 38。 match 表达式获取 Ordering::Greater 值,并开始检查每个arm的模式。它查看了第一个arm的模式 Ordering::Less ,发现 Ordering::Greater 的值与 Ordering::Less 不匹配,因此忽略了该臂中的代码,转到下一个arm。下一个arm的模式是 Ordering::Greater ,它确实与 Ordering::Greater 匹配!该臂中的相关代码将执行并将 Too big! 打印到屏幕上。 match 表达式在第一次成功匹配后结束,因此在这种情况下不会查看最后一个arm。
不过,上述的代码还无法编译。让我们试试看:
cargo.exe buildCompiling guessing_game v0.1.0 (D:\rustProj\guessing_game)
error[E0308]: mismatched types--> src\main.rs:19:21|
19 | 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/82e1608dfa6e0b5569232559e3d385fea5a93112\library\core\src\cmp.rs:811:8For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 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 函数体中添加了这一行:
use std::io;
use rand::Rng;
use std::cmp::Ordering;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!("Your 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 之前的值进行阴影处理。阴影允许我们重复使用 guess 变量名,而不是强迫我们创建两个唯一的变量,例如 guess_str 和 guess 。当你想将一个值从一种类型转换为另一种类型时,经常会用到这个功能。
这点和C、C++不太一样;
我们将这个新变量绑定到表达式 guess.trim().parse() 上。表达式中的 guess 指的是包含字符串输入的原始 guess 变量。 String 实例上的 trim 方法将消除开头和结尾的空白,我们必须这样做才能将字符串与 u32 进行比较,后者只能包含数字数据。用户必须按回车键才能满足 read_line ,并输入他们的猜测,这将在字符串中添加一个换行符。例如,如果用户输入 5 并按回车键, guess 就会变成这样: 5\n 。 \n 代表 "换行"。(在 Windows 系统中,按回车键的结果是回车和换行符,即 \r\n 。) trim 方法消除了 \n 或 \r\n ,结果只有 5 。
字符串的 parse 方法可将字符串转换为另一种类型。在这里,我们用它将字符串转换为数字。我们需要使用 let guess: u32 告诉 Rust 我们想要的确切数字类型。 guess 后面的冒号 ( : ) 告诉 Rust 我们将注释变量的类型。Rust 有几种内置的数字类型;这里看到的 u32 是一个无符号的 32 位整数。对于小正数来说,这是一个不错的默认选择。
变量命名和kotlin很像....
val guess: Int
此外,示例程序中的 u32 注释以及与 secret_number 的比较意味着 Rust 将推断 secret_number 也应该是 u32 。因此,现在将在两个相同类型的值之间进行比较!
parse 方法只适用于逻辑上可以转换成数字的字符,因此很容易出错。例如,如果字符串包含 A👍% ,就无法将其转换为数字。因为可能会失败,所以 parse 方法会返回一个 Result 类型,就像 read_line 方法一样。我们将再次使用 expect 方法,以同样的方式处理 Result 。如果 parse 因无法从字符串中创建数字而返回 Err,那么 expect 调用将使程序崩溃,并打印出我们给它的信息。如果 parse 能够成功地将字符串转换为数字,它将返回 Ok,而 expect 将从 Ok 值返回我们想要的数字。
现在运行程序:
cargo.exe runFinished dev [unoptimized + debuginfo] target(s) in 0.03sRunning `target\debug\guessing_game.exe`
Guess the number!
The secret number is:12
Please input your guess.
56
Your guessed: 56
Too big
我们现在已经完成了大部分游戏,但用户只能猜一个数字。让我们通过添加一个循环来改变这种情况!
4. 通过循环允许多次猜测
loop 关键字会创建一个无限循环。我们将添加一个循环,让用户有更多机会猜出数字:
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!("Your guessed: {guess}");match guess.cmp(&secret_number) {Ordering::Less => println!("Too small"),Ordering::Greater => println!("Too big"),Ordering::Equal => println!("You win"),}}
}
如你所见,我们将从猜测输入提示开始的所有内容都移到了一个循环中。现在程序将永远要求用户再猜一次,这实际上带来了一个新问题。用户似乎无法退出!
4.1 猜对后退出
让我们在程序中加入 break 语句,使游戏在用户获胜后退出:
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!("Your 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 行,可使程序在用户正确猜出秘密号码后退出循环。退出循环也意味着退出程序,因为循环是 main 的最后一部分。
4.2 处理无效输入
为了进一步完善游戏行为,我们可以让游戏忽略非数字,这样用户就可以继续猜谜,而不是当用户输入非数字时程序崩溃。我们可以修改 guess 从 String 转换为 u32 的行:
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!("Your 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 返回 Result 类型,而 Result 是一个枚举,有 Ok 和 Err 变量。我们在这里使用的是 match 表达式,就像使用 cmp 方法的 Ordering 结果一样。
如果 parse 能够成功地将字符串转化为数字,它将返回一个包含结果数字的 Ok 值。 Ok 值将与第一arm的模式匹配,而 match 表达式将只返回 parse 生成的 num 值,并将其放入 Ok 值内。这个数字最终会出现在我们要创建的新变量 guess 中。
如果 parse 无法将字符串转换成数字,它将返回一个包含更多错误信息的 Err 值。 Err 值与 match 第一arm中的 Ok(num) 模式不匹配,但与第二arm中的 Err(_) 模式匹配。下划线 _ 是一个总括值;在这个示例中,我们表示要匹配所有 Err 值,无论它们包含什么信息。因此,程序将执行第二arm的代码 continue ,它告诉程序进入 loop 的下一个迭代,并要求再次猜测。因此,程序实际上忽略了 parse 可能遇到的所有错误!
至此,已经成功构建了猜谜游戏;
下一篇: 03-常用编程概念