对猜谜游戏进行编程
让我们一起完成一个动手项目,进入 Rust!这
本章介绍了一些常见的 Rust 概念,向您展示了如何使用
他们在一个真实的程序中。您将了解let
,match
、 方法、 关联
函数、外部 crate 等等!在接下来的章节中,我们将探讨
这些想法更详细。在本章中,您将只练习
基础。
我们将实现一个经典的初学者编程问题:猜谜游戏。这是 运作方式: 该程序将生成一个介于 1 和 100 之间的随机整数。它 然后会提示玩家输入猜测。输入猜测后, 程序将指示猜测值是太低还是太高。如果猜测是 正确,游戏将打印祝贺消息并退出。
设置新项目
要设置新项目,请转到您在中创建的项目目录 第 1 章,并使用 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
Command 在您需要快速迭代项目时派上用场,
正如我们在这个游戏中所做的那样,在继续
下一个。
重新打开 src/main.rs 文件。您将在此文件中编写所有代码。
处理猜测
猜谜游戏程序的第一部分会要求用户输入,处理 该 input,并检查输入是否为预期形式。首先,我们将 允许玩家输入猜测。将示例 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
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,而 您可以在 Standard Library 文档中看到其中的所有内容。
如果要使用的类型不在 prelude 中,则必须引入该类型
into 范围,并带有use
陈述。使用std::io
图书馆
为您提供了许多有用的功能,包括接受
用户输入。
正如您在第 1 章中看到的,main
function 是
程序:
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
syntax 声明一个新函数;括号 , 表示那里
没有参数;大括号 , 开始函数的主体。()
{
正如您在第 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 会忽略注释中的所有内容。我们将在更多内容中讨论评论
detail 在第 3 章中。//
回到猜谜游戏程序,您现在知道了let mut guess
将
引入一个名为guess
.等号 () 告诉 Rust 我们
想要现在将某些内容绑定到变量。等号的右侧是
该值=
guess
绑定到,这是调用String::new
中,该函数返回String
.String
是标准
库,它是一个可增长的 UTF-8 编码文本位。
这::
语法::new
line 表示new
是关联的
函数的String
类型。关联的函数是
在本例中为 type 上实现String
.这new
函数创建一个
new 的空字符串。您将找到一个new
函数,因为它是一个
生成某种新值的函数的 common name 。
总的来说,let mut guess = String::new();
line 创建了一个可变的
变量,该变量当前绑定到String
.呼!
接收用户输入
回想一下,我们从标准中包含了输入/输出功能
library 替换为use std::io;
在程序的第一行。现在我们调用
这stdin
函数io
模块,这将允许我们处理
输入:
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
library 替换为use std::io;
在
程序中,我们仍然可以通过将这个函数调用编写为std::io::stdin
.这stdin
function 返回std::io::Stdin
,该类型表示
handle 添加到终端的标准输入端。
接下来,这条线.read_line(&mut guess)
调用read_line
方法从用户那里获取输入。
我们也在传递&mut guess
作为参数read_line
告诉它什么
string 来存储用户输入。全部工作read_line
是拿
用户在 Standard Input 中键入的任何内容并将其附加到字符串中
(而不覆盖其内容),因此我们将该字符串作为
论点。string 参数需要是可变的,以便方法可以更改
String 的内容。
这表示此参数是一个引用,这为您提供了一种
允许代码的多个部分访问一条数据,而无需
多次将该数据复制到内存中。引用是一个复杂的功能,
Rust 的主要优势之一是它的安全性和易用性
引用。您无需了解很多细节即可完成此作
程序。现在,您需要知道的是,与变量一样,引用也是
默认为 immut。因此,您需要编写&
&mut guess
而不是&guess
使其可变。(第 4 章将更多地解释参考资料
彻底。
处理潜在故障Result
我们仍在处理这行代码。我们现在讨论的是 text 中,但请注意,它仍然是单个逻辑代码行的一部分。下一个 部分是这个方法:
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
是一个枚举,通常称为枚举,
该类型可以处于多种可能的状态之一。我们称每个
possible 状态 a 变体。
第 6 章将更详细地介绍枚举。目的
其中Result
types 用于对错误处理信息进行编码。
Result
的变体是Ok
和Err
.这Ok
variant 表示
作成功,并在Ok
是成功生成的值。
这Err
variant 表示作失败,而Err
包含信息
了解作失败的方式或原因。
的值Result
type 与任何类型的值一样,在
他们。一个Result
具有expect
方法你可以打电话。如果此Result
是一个Err
价值expect
将导致程序崩溃并显示您作为
argument 设置为expect
.如果read_line
method 返回一个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 警告您尚未使用Result
值返回自read_line
,
指示程序尚未处理可能的错误。
抑制警告的正确方法是实际编写错误处理代码
但是在我们的例子中,我们只想在出现问题时使这个程序崩溃,所以我们
可以使用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);
}
此行打印现在包含用户输入的字符串。的集合
大括号是一个占位符:可以想象成小螃蟹钳
一个值就位。打印变量的值时,变量名称可以
进入大括号内。打印
表达式,请在格式字符串中放置空的大括号,然后按照
format 字符串,其中包含要在每个空函数中打印的以逗号分隔的表达式列表
大括号占位符。打印变量和结果
的表达式{}
{}
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
.
测试 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 是 Rust 源代码文件的集合。项目
我们一直在构建一个二进制 crate,它是一个可执行文件。这rand
crate 是一个库 crate,它包含了打算在
其他程序,不能单独执行。
Cargo 对外部板条箱的协调是 Cargo 真正闪耀的地方。在我们之前
可以编写使用rand
,我们需要将 Cargo.toml 文件修改为
包括rand
crate 作为依赖项。现在打开该文件并添加
以下行到底部,在[dependencies]
section 标头
为您创建的 Cargo。请务必指定rand
正如我们在这里看到的一样,使用
此版本号或本教程中的代码示例可能不起作用:
文件名: Cargo.toml
[dependencies]
rand = "0.8.5"
在 Cargo.toml 文件中,标头后面的所有内容都是该文件的一部分
部分继续,直到另一个部分开始。在[dependencies]
你
告诉 Cargo 你的项目依赖于哪些外部 crate 以及哪些版本的
您需要的那些板条箱。在本例中,我们指定rand
crate 替换为
语义版本说明符0.8.5
.Cargo 理解语义
版本控制(有时称为 SemVer),它是一个
编写版本号的标准。说明符0.8.5
其实
的简写^0.8.5
,这意味着任何版本至少为 0.8.5 但
低于 0.9.0。
Cargo 认为这些版本具有与版本 0.8.5 版本,并且此规范可确保您获得最新的补丁版本,该 仍会使用本章中的代码进行编译。任何版本 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
cargo build
将 Rand crate 添加为依赖项后您可能会看到不同的版本号(但它们都将与 代码,感谢 SemVer!和不同的线路(取决于作 system),并且这些行的顺序可能不同。
当我们包含外部依赖项时,Cargo 会获取最新版本的 依赖项需要从注册表中获得的所有内容,即数据副本 从 Crates.io。Crates.io 是 Rust 生态系统中人们的地方 发布他们的开源 Rust 项目供其他人使用。
更新注册表后,Cargo 会检查[dependencies]
部分和
下载列出的任何尚未下载的 crate。在这种情况下,
虽然我们只列出了rand
作为依赖项,Cargo 还抢走了其他板条箱
那rand
取决于工作。下载 crate 后,Rust 编译
them,然后使用可用的依赖项编译项目。
如果您立即运行cargo build
同样,无需进行任何更改,您
不会获得除Finished
线。Cargo 知道它已经
下载并编译了依赖项,您没有更改任何内容
关于 Cargo.toml 文件中的 Package。Cargo 也知道您没有更改
任何关于你的代码的信息,所以它也不会重新编译它。无所事事
do,它只是退出。
如果你打开 src/main.rs 文件,做一个小小的修改,然后保存它并 build 再次构建,您将只看到两行输出:
$ 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 将仅使用
dependencies 的 trust 选项,直到您另有说明。例如,假设
下周 0.8.6 版rand
crate 出来了,那个版本
包含一个重要的错误修复,但它也包含一个回归,该回归将
破解你的代码。为了解决这个问题,Rust 首先创建了 Cargo.lock 文件
你跑的时间cargo build
,所以我们现在在 guessing_game 目录中有这个。
当你第一次构建一个项目时,Cargo 会找出所有版本 ,然后将它们写入 Cargo.lock 文件。当你将来构建项目时,Cargo 将看到 Cargo.lock 文件存在并将使用其中指定的版本 而不是再次做所有弄清楚版本的工作。这让您 自动获得可重现的构建。换句话说,您的项目将 保持 0.8.5 状态,直到您明确升级,这要归功于 Cargo.lock 文件。 由于 Cargo.lock 文件对于可重现的构建非常重要,因此它通常是 签入 源代码管理,并将项目的其余代码包含在 Source Control 中。
更新 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
现在使用的是 IS 0.8.6。要使用rand
版本 0.9.0 或 0.9 中的任何版本。x 系列,则必须更新 Cargo.toml 文件,使其如下所示:
[dependencies]
rand = "0.9.0"
下次运行cargo build
,Cargo 将更新 crate 的注册表
available 并重新评估您的rand
根据新版本的要求
您已指定。
关于 Cargo 及其 ecosystem,我们将在第 14 章中讨论,但是 现在,这就是您需要了解的全部内容。Cargo 使其非常容易重复使用 库,因此 Rustacean 能够编写组装的较小项目 从多个包中。
生成随机数
让我们开始使用rand
生成一个数字进行猜测。下一步是
update src/main.rs,如示例 2-3 所示。
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 章将详细介绍特征。
接下来,我们在中间添加两行。在第一行中,我们调用rand::thread_rng
函数,它为我们提供了特定的随机数
生成器:一个位于当前线程
执行,并由作系统作为种子。然后我们调用gen_range
方法。此方法由Rng
trait 中引入范围的 traituse rand::Rng;
陈述。这gen_range
method 将 Range 表达式作为参数,并生成一个
范围内的随机数。我们在此处使用的范围表达式类型采用
表单start..=end
并且包含下限和上限,因此我们
需要指定1..=100
请求一个介于 1 和 100 之间的数字。
注意:您不仅会知道要使用哪些 trait 以及哪些方法和函数
从 crate 调用,因此每个 crate 都有文档,其中包含
使用它。Cargo 的另一个巧妙特性是运行cargo doc --open
command 将构建由您的所有依赖项提供的文档
本地并在浏览器中打开它。如果您对其他
功能中的rand
crate,请运行cargo doc --open
和
点击rand
在左侧边栏中。
第二个新行打印秘密号码。这在我们 开发程序以便能够对其进行测试,但我们会将其从 final 版本。如果程序立即打印答案,那就没什么大不了的了 当它开始时!
尝试运行该程序几次:
$ 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 所示。请注意,这段代码还不会编译,因为我们会编译 解释。
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
into 范围。这Ordering
类型
是另一个枚举,并且具有Less
,Greater
和Equal
.这些是
比较两个值时可能出现的三种结果。
然后我们在底部添加 5 行使用Ordering
类型。这cmp
方法比较两个值,并且可以对任何可以
比较。它需要一个对你想要比较的任何内容的引用:这里是
比较guess
自secret_number
.然后,它返回Ordering
enum 中,我们使用use
陈述。我们使用match
表达式来决定下一步做什么
的哪个变体Ordering
从对cmp
替换为值
在guess
和secret_number
.
一个match
表情由手臂组成。手臂由一个模式组成,以
match 对,如果给定的值为match
适合那只手臂的模式。Rust 将给定的值match
和外观
依次通过每个手臂的模式。Patterns 和match
construct 是
强大的 Rust 功能:它们允许您表达代码中的各种情况
可能会遇到,他们确保您处理所有问题。这些功能将是
分别在第 6 章和第 18 章中详细介绍。
让我们来看一个带有match
表达式。说那个
用户猜出了 50,这次随机生成的秘密号码是
38.
当代码将 50 与 38 进行比较时,cmp
method 将返回Ordering::Greater
因为 50 大于 38。这match
表达式获取
这Ordering::Greater
值并开始检查每个臂的模式。看起来
在第一臂的模式中,Ordering::Less
,并看到值Ordering::Greater
不匹配Ordering::Less
,因此它会忽略
该手臂并移动到下一个手臂。下一个分支的模式是Ordering::Greater
,该 ID 与Ordering::Greater
!关联的
该 ARM 中的代码将执行并打印Too big!
拖动到屏幕上。这match
表达式在第一次成功匹配后结束,因此它不会查看最后一个
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
,另一方面
hand,是一个数字类型。一些 Rust 的数字类型的值可以介于 1 之间
和 100:i32
、32 位数字;u32
、无符号的 32 位数字;i64
一个
64 位数字;以及其他一些。除非另有说明,否则 Rust 默认为
一i32
,这是secret_number
除非添加类型信息
否则会导致 Rust 推断出不同的数值类型。原因
因为错误是 Rust 无法比较字符串和数字类型。
最终,我们希望将String
程序将作为输入读取到
number 类型,以便我们可以将其与秘密数字进行数值比较。我们通过以下方式实现
将此行添加到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 允许我们隐藏
的 Previous 值guess
替换为新的。Shadowing 允许我们重用guess
变量名称,而不是强制我们创建两个唯一的变量,例如guess_str
和guess
例如。我们将在第 3 章中更详细地介绍这一点,但现在,要知道这个功能是
当您想要将值从一种类型转换为另一种类型时,通常会使用。
我们将这个新变量绑定到表达式guess.trim().parse()
.这guess
在表达式中引用原始guess
变量,其中包含
input 作为字符串。这trim
方法在String
instance 将消除任何
whitespace 的开头和结尾,我们必须这样做才能比较
string 添加到u32
,它只能包含数值数据。用户必须按下才能满足enterread_line
并输入他们的猜测,这会添加一个
换行符添加到字符串中。例如,如果用户键入
印刷机5enterguess
如下所示:5\n
.这\n
代表
“换行符。”(在 Windows 上,按 会导致回车
和一个换行符enter\r\n
.)这trim
方法消除\n
或\r\n
导致
只是5
.
这parse
字符串上的 method将字符串转换为
另一种类型。在这里,我们使用它从字符串转换为数字。我们需要
通过使用let guess: u32
.冒号
(:
) 之后guess
告诉 Rust 我们将注释变量的类型。Rust 有一个
很少有内置数字类型;这u32
这里看到的是一个无符号的 32 位整数。
对于较小的正数,这是一个不错的默认选择。您将了解
第 3 章中的其他数字类型。
此外,u32
注解和比较
跟secret_number
意味着 Rust 会推断出secret_number
应为u32
也。所以现在比较将是两个相同的值
类型!
这parse
方法仅适用于逻辑上可以转换的字符
转换为数字,因此很容易导致错误。例如,如果字符串
包含A👍%
,则无法将其转换为数字。因为它
可能会失败,则parse
method 返回一个Result
type 的read_line
方法执行“处理潜在故障Result
”).我们将处理
这Result
以同样的方式使用expect
方法。如果parse
返回一个Err
Result
变体,因为它无法从
string 中,该expect
call 将使游戏崩溃并打印我们给它的消息。
如果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
keyword 创建一个无限循环。我们将添加一个循环,为用户提供
更多猜数字的机会:
文件名: 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 提示符中的所有内容移至 一个循环。确保循环内的行再缩进四个空格 并再次运行该程序。程序现在将永远要求再次猜测, 这实际上引入了一个新问题。用户似乎无法退出!
用户始终可以使用键盘快捷键 - 中断程序。但还有另一种方法可以摆脱这种贪得无厌
monster 的ctrlcparse
“将 Guess 与
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;
}
}
}
}
添加break
行后You win!
使程序退出循环
用户猜对了密钥号码。退出循环也意味着
退出程序,因为循环是main
.
处理无效输入
为了进一步优化游戏的行为,而不是在
用户输入一个非数字,让我们让游戏忽略一个非数字,这样
用户可以继续猜测。我们可以通过更改guess
从String
更改为u32
,如示例 2-5 所示。
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
要从崩溃中移动的表达式
on an error 处理错误。请记住parse
返回Result
type 和Result
是一个枚举,其变体Ok
和Err
.我们正在使用
一个match
表达式,就像我们对Ordering
的结果cmp
方法。
如果parse
能够成功地将字符串转换为数字,它将
返回一个Ok
值。那Ok
value 将
匹配第一个臂的模式,并且match
expression 将只返回num
值parse
生产并放入Ok
价值。那个数字
将出现在我们想要的新guess
变量。
如果parse
无法将字符串转换为数字,则会返回一个Err
值,其中包含有关错误的更多信息。这Err
价值
与Ok(num)
pattern 中的第一个match
arm 的
匹配Err(_)
模式。下划线 , 是一个
catchall 值;在此示例中,我们表示要匹配所有_
Err
值,无论它们里面有什么信息。所以程序将
执行第二个 Arm 的代码continue
,它告诉程序转到
的 next iterationloop
并要求再次猜测。因此,实际上,
程序会忽略所有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 显示了最终的代码。
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 章解释了枚举的工作原理。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准