02-编程猜谜游戏
本章通过演示如何在实际程序中使用 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 可能遇到的所有错误!
至此,已经成功构建了猜谜游戏;
相关文章:

02-编程猜谜游戏
本章通过演示如何在实际程序中使用 Rust,你将了解 let 、 match 、方法、关联函数、外部crate等基础知识。 本章将实现一个经典的初学者编程问题:猜谜游戏。 工作原理如下:程序将随机生成一个介于 1 和 100 之间的整数。然后,程序…...

Web3解密:区块链技术如何颠覆传统互联网
随着区块链技术的崛起,Web3正逐渐成为新一代互联网的代名词。它不再依赖中心化的权威机构,而是通过去中心化、透明、安全的特性,为用户带来更为开放和公正的互联网体验。本文将深入解密Web3,揭示区块链技术如何颠覆传统互联网的基…...

java小项目:简单的收入明细记事本,超级简单(不涉及数据库,通过字符串来记录)
一、效果 二、代码 2.1 Acount类 package com.demo1;public class Acount {public static void main(String[] args) {String details "收支\t账户金额\t收支金额\t说 明\n"; //通过字符串来记录收入明细int balance 10000;boolean loopFlag true;//控制循…...

域环境权限提升
Windows系统配置错误 在Windows系统中,攻击者通常会通过系统内核溢出漏来提权,但是如果碰到无法通过系统内核溢出漏洞法国提取所在服务器权限的情况,就会系统中的配置错误来提权。Windows系统中常见哦欸之错误包括管理员凭证配置错误&#x…...

【Debian】非图形界面Debian10.0.0安装xfce和lxde桌面
一、安装 1. Debian10.0.0安装xfce桌面 sudo apt update sudo apt install xfce4 startxfce4 2. Debian10.0.0安装lxde桌面 sudo apt-get install lxde安装后重启电脑。 二、说明 XFCE、LXDE 和 GNOME 是三个流行的桌面环境,它们都是为类 Unix 操作系统设计…...

极狐GitLab 线下『 DevOps专家训练营』成都站开班在即
成都机器人创新中心联合极狐(GitLab)隆重推出极狐GitLab DevOps系列认证培训课程。该课程主要面向使用极狐GitLab的DevOps工程师、安全审计人员、系统运维工程师、系统管理员、项目经理或项目管理人员,完成该课程后,学员将达到DevOps的专家级水平&#x…...

片外存储器_FLASH的页、扇区、块介绍
目录标题 1、什么是FLASH存储器?2、Flash中页、扇区、块是什么意思?有什么区别?2.1、芯片内部框图2.2、页2.3、扇区2.4、块2.5、包含示意图 3、使用FLASH 时候,必须知道的事。 1、什么是FLASH存储器? FLASH属于广义的ROM&#x…...

Python——字符串的基本操作
⼀、 创建 s1 lenovo s2 "QF" s3 """hello lenovo""" s4 hello 亮 s5 """hello shark """ s6 hello world ⼆、简单使⽤ 1.\ 转义符 testimony This shirt doesn\t fit me words hello \ns…...

【cuda】四、基础概念:Cache Tiled 缓存分块技术
缓存分块是一种内存优化技术,主要用于提高数据的局部性(Locality),以减少缓存未命中(Cache Miss)的次数。在现代计算机体系结构中,处理器(CPU)的速度通常比内存快得多。因…...

[C#]winform部署openvino官方提供的人脸检测模型
【官方框架地址】 https://github.com/sdcb/OpenVINO.NET 【框架介绍】 OpenVINO(Open Visual Inference & Neural Network Optimization)是一个由Intel推出的,针对计算机视觉和机器学习任务的开源工具套件。通过优化神经网络ÿ…...

Java中对日期的处理
Java中对日期的处理 这个案例主要掌握: 1.怎么获取系统当前时间 2.String-->Date 3.Date-->String Import java.text.SimpleDateFormat; Import java.util.Date; public class DateTest01{ public static void main(String[] args) throws Exception{ //获取…...

【Linux install】Ubuntu和win双系统安装及可能遇到的所有问题
文章目录 1.前期准备1.1 制作启动盘1.2关闭快速启动、安全启动、bitlocker1.2.1 原因1.2.2 进入BIOSshell命令行进入BIOSwindows设置中高级启动在开机时狂按某个键进入BIOS 1.2.3 关闭Fast boot和Secure boot 1.3 划分磁盘空间1.3.1 查看目前的虚拟内存大小 2.开始安装2.1 使用…...

Helm Dashboard — Kubernetes 中管理 Helm 版本的 GUI
Helm Dashboard 通过提供图形用户界面,使在 Kubernetes 中管理 Helm 版本变得更加容易,这是许多开发人员所期望的。它可用于在 Kubernetes 中创建、部署和更新应用程序的版本,并跟踪其状态。 本文将探讨 Helm Dashboard 提供的特性和优势&am…...

【Guava笔记01】Guava Cache本地缓存的常用操作方法
这篇文章,主要介绍Guava Cache本地缓存的常用操作方法。 目录 一、Guava Cache本地缓存 1.1、引入guava依赖 1.2、CacheBuilder类 1.3、Guava-Cache使用案例...

Flink(十三)【Flink SQL(上)SqlClient、DDL、查询】
前言 最近在假期实训,但是实在水的不行,三天要学完SSM,实在一言难尽,浪费那时间干什么呢。SSM 之前学了一半,等后面忙完了,再去好好重学一遍,毕竟这玩意真是面试必会的东西。 今天开始学习 Flin…...

Labview局部变量、全局变量、引用、属性节点、调用节点用法理解及精讲
写本章前想起题主初学Labview时面对一个位移台程序,傻傻搞不清局部变量和属性节点值有什么区别,概念很模糊。所以更新这篇文章让大家更具象和深刻的去理解这几个概念,看完记得点赞加关注喔~ 本文程序源代码附在后面,大家可以自行下…...

openssl3.2 - 官方demo学习 - signature - EVP_ED_Signature_demo.c
文章目录 openssl3.2 - 官方demo学习 - signature - EVP_ED_Signature_demo.c概述笔记END openssl3.2 - 官方demo学习 - signature - EVP_ED_Signature_demo.c 概述 ED25519 签名/验签算法, 现在是最好的. 产生ED25519私钥/公钥 用私钥对明文签名, 得到签名数据 用公钥对明文…...

AI辅助编程工具—Github Copilot
一、概述 Copilot是一种基于Transformer模型的神经网络,具有12B个参数。是GitHub和OpenAPI共同开发的编程辅助工具。GitHubCopilot是一款由人工智能驱动的结对编程编辑器,旨在帮助开发人员更加高效地工作。它利用OpenAICodex技术,将开发…...

三大3D引擎对比,直观感受AMRT3D渲染能力
作为当前热门的内容呈现形式,3D已经成为了广大开发者、设计师工作里不可或缺的一部分。 用户对于3D的热衷,源于其带来的【沉浸式体验】和【超仿真视觉效果】。借此我们从用户重点关注的四个3D视觉呈现内容: 材质- 呈现多元化内容水效果- 展…...

k8s之对外服务ingress
一、service 1、service作用 ①集群内部:不断跟踪pod的变化,不断更新endpoint中的pod对象,基于pod的IP地址不断变化的一种服务发现机制(endpoint存储最终对外提供服务的IP地址和端口) ②集群外部:类似负…...

Ubuntu使用docker-compose安装mysql8或mysql5.7
ubuntu环境搭建专栏🔗点击跳转 Ubuntu系统环境搭建(十四)——使用docker-compose安装mysql8或mysql5.7 文章目录 Ubuntu系统环境搭建(十四)——使用docker-compose安装mysql8或mysql5.7MySQL81.新建文件夹2.创建docke…...

【办公类-21-02】20240118育婴员操作题word打印2.0
作品展示 把12页一套的操作题批量制作10份,便于打印 背景需求 将昨天整理的育婴师操作题共享, 因为题目里面有大量的红蓝颜色文字,中大班办公室都是黑白单面手动翻页打印。只有我待的教务室办公室有彩色打印机打印(可以自动双面…...

SpringMVC 文件上传和下载
文章目录 1、文件下载2、文件上传3. 应用 Spring MVC 提供了简单而强大的文件上传和下载功能。 下面是对两者的简要介绍: 文件上传: 在Spring MVC中进行文件上传的步骤如下: 在表单中设置 enctype“multipart/form-data”,这样…...

强缓存、协商缓存(浏览器的缓存机制)是么子?
文章目录 一.为什么要用强缓存和协商缓存?二.什么是强缓存?三.什么是协商缓存?四.总结 一.为什么要用强缓存和协商缓存? 为了减少资源请求次数,加快资源访问速度,浏览器会对资源文件如图片、css文件、js文…...

android 13.0 Camera2 去掉后置摄像头 仅支持前置摄像头功能
1.概述 在定制化13.0系统rom定制化开发中,当产品只有一个前置摄像头单摄像头,这时调用相机时就需要默认打开前置摄像头就需要来看调用摄像头这块的代码,屏蔽掉后置摄像头的调用api就可以了,接下来就来具体实现相关功能的开发 2.Camera2 去掉后置摄像头 仅支持前置摄像头功…...

【蓝桥杯EDA设计与开发】立创开源社区分享的关于蓝桥被EDA真题与仿真题的项目分析
立创开源社区内有几个项目分享了往年 EDA 设计题目与仿真题,对此展开了学习。 【本人非科班出身,以下对项目的学习仅在我的眼界范围内发表意见,如有错误,请指正。】 项目一 来源:第十四届蓝桥杯EDA赛模拟题一 - 嘉立…...

电影《潜行》中说的蜜罐是什么(网络安全知识)
近期刘德华、彭于晏主演的电影《潜行》在网上掀起了轩然大波,电影中有提到网络蜜罐,这引起了很多观众的疑问,蜜罐到底是什么? 从字面意思上来看,蜜罐就是为黑客设下的诱饵。这是一种具有牺牲性质的计算机系统ÿ…...

基于 UniAPP 社区论坛项目多端开发实战
社区论坛项目多端开发实战 基于 UniAPP 社区论坛项目多端开发实战一、项目准备1.1 ThinkSNS 简介及相关文档1.2 使用 UniAPP 构建项目1.3 构建项目文件结构1.4 配置页面 TabBar 导航1.5 使用 npm 引入 uView UI 插件库 二、首页功能实现2.1 首页 header 广告位轮播图功能实现2.…...

Ubuntu 22.04 安装MySql
MySQL是非常常用的关系型数据库,无论是大厂还是小厂,都有它的身影。最大的优点是免费,安装起来也比较简单。 MySQL的架构 画了个简图,描述了下MySQL的架构。 其中的比较有趣的点在于连接池和存储引擎。连接池缓存了数据库和客户端的TCP连接,以减少建立连接的开销。存储引…...

Centos常用命令整理,常用的比较全了
目 录 1、更改文件拥有者 2、修改权限 3、修改⽂件⽇期 4、链接⽂件 5、⽇期操作 6、显⽰⽇历 7、显⽰⽂件头部 8、显⽰⽂件尾部 9、显⽰⽤户标识 10、查看当前登录的⽤户 11、显⽰都谁登录到机器上 12、显⽰当前终端上的⽤户名 13、寻找⽂件…...