update ch02-00

This commit is contained in:
KaiserY 2024-08-19 18:15:52 +08:00
parent fccbde86b4
commit 7833ead59b

View File

@ -2,11 +2,11 @@
> [ch02-00-guessing-game-tutorial.md](https://github.com/rust-lang/book/blob/main/src/ch02-00-guessing-game-tutorial.md)
> <br>
> commit f28554d1b216d49a4d11d05995302cf54a0f9d72
> commit 11ca3d508b0a28b03f7d9f16c88726088fafd87e
让我们一起动手完成一个项目,来快速上手 Rust本章将介绍 Rust 中一些常用概念,并通过真实的程序来展示如何运用它们。你将会学到 `let`、`match`、方法method、关联函数associated function、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将练习基础内容。
让我们一起动手完成一个项目来快速上手 Rust本章将介绍一些 Rust 中常见的概念,并通过真实的程序来展示如何运用它们。你将会学到 `let`、`match`、方法methods、关联函数associated functions)、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将主要练习基础内容。
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
我们会实现一个经典的新手编程问题:猜数字游戏。游戏的规则如下:程序将会生成一个 1 到 100 之间的随机整数。然后提示玩家输入一个猜测值。输入后,程序会指示该猜测是太低还是太高。如果猜对了,游戏会打印祝贺信息并退出。
## 准备一个新项目
@ -47,17 +47,17 @@ $ cd guessing_game
## 处理一次猜测
猜看程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许玩家输入猜测。在 _src/main.rs_ 中输入示例 2-1 中的代码。
数字程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,我们会允许玩家输入一个猜测。在 _src/main.rs_ 中输入示例 2-1 中的代码。
<span class="filename">文件名src/main.rs</span>
<Listing number="2-1" file-name="src/main.rs" caption="获取用户猜测并打印的代码">
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:all}}
```
<span class="caption">示例 2-1获取用户猜测并打印的代码</span>
</Listing>
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io`输入/输出库引入当前作用域。`io` 库来自于标准库,也被称为 `std`
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io` 输入/输出库引入当前作用域。`io` 库来自于标准库,也被称为 `std`
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:io}}
@ -106,25 +106,23 @@ let mut bananas = 5; // 可变
> 注意:`//` 语法开始一个注释持续到行尾。Rust 忽略注释中的所有内容,[第三章][comments]将会详细介绍注释。
回到猜猜看程序中。现在我们知道了 `let mut guess` 会引入一个叫做 `guess` 的可变变量。等号(`=`)告诉 Rust 我们现在想将某个值绑定在变量上。等号的右边是 `guess` 所绑定的值,它是 `String::new` 的结果,这个函数会返回一个 `String` 的新实例。[`String`][string]<!-- ignore --> 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
回到猜数字程序中。现在我们知道了 `let mut guess` 会引入一个叫做 `guess` 的可变变量。等号(`=`)告诉 Rust 我们现在想将某个值绑定在变量上。等号的右边是 `guess` 所绑定的值,它是 `String::new` 的结果,这个函数会返回一个 `String` 的新实例。[`String`][string]<!-- ignore --> 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
`::new` 那一行的 `::` 语法表明 `new``String` 类型的一个 **关联函数**_associated function_。关联函数是针对类型实现的在这个例子中是 `String`,而不是 `String` 的某个特定实例。一些语言中把它称为 **静态方法**_static method_
`new` 函数创建了一个新的空字符串,你会发现很多类型上有 `new` 函数,因为它是创建类型实例的惯用函数名。
`::new` 那一行的 `::` 语法表明 `new``String` 类型的一个 **关联函数**_associated function_。关联函数是针对某个类型实现的函数在这个例子中是 `String`。这个 `new` 函数创建了一个新的空字符串。你会发现许多类型上都有一个 `new` 函数,因为这是为某种类型创建新值的常用函数名。
总的来说,`let mut guess = String::new();` 这一行创建了一个可变变量,当前它绑定到一个新的 `String` 空实例上。
### 接收用户输入
回忆一下,我们在程序的第一行使用 `use std::io;` 从标准库中引入了输入/输出功能。现在调用 `io` 库中的函数 `stdin`
回忆一下,我们在程序的第一行使用 `use std::io;` 从标准库中引入了输入/输出功能。现在调用 `io` 库中的函数 `stdin`,这允许我们处理用户输入
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:read}}
```
如果程序的开头没有使用 `use std::io;` 引入 `io` 库,我们仍可以通过把函数调用写成 `std::io::stdin` 来使用函数。`stdin` 函数返回一个 [`std::io::Stdin`][iostdin]<!-- ignore --> 的实例,这代表终端标准输入句柄的类型。
如果程序的开头没有使用 `use std::io;` 引入 `io` 库,我们仍可以通过把函数调用写成 `std::io::stdin` 来使用函数。`stdin` 函数返回一个 [`std::io::Stdin`][iostdin]<!-- ignore --> 的实例,这是一种代表终端标准输入句柄的类型。
代码的下一部分,`.read_line(&mut guess)`,调用 [`read_line`][read_line]<!-- ignore --> 方法从标准输入句柄获取用户输入。我们还将 `&mut guess` 作为参数传递给 `read_line()` 函数,让其将用户输入储存到这个字符串中。`read_line` 的工作是,无论用户在标准输入中键入什么内容,都将其追加(不会覆盖其原有内容)到一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 `read_line` 将用户输入附加上去。
接下来,代码中的 `.read_line(&mut guess)` 调用了标准输入句柄上的 [`read_line`][read_line]<!-- ignore --> 方法,以获取用户输入。我们还将 `&mut guess` 作为参数传递给 `read_line` 函数,让其将用户输入储存到这个字符串中。`read_line` 的工作是,无论用户在标准输入中键入什么内容,都将其追加(不会覆盖其原有内容)到一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 `read_line` 将用户输入附加上去。
`&` 表示这个参数是一个 **引用**_reference_它允许多处代码访问同一处数据而无需在内存中多次拷贝。引用是一个复杂的特性Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 `&mut guess` 来使其可变,而不是 `&guess`。(第四章会更全面的解释引用。)
@ -149,7 +147,7 @@ io::stdin().read_line(&mut guess).expect("Failed to read line");
[第六章][enums]将介绍枚举的更多细节。这里的 `Result` 类型将用来编码错误处理的信息。
`Result` 的成员是 `Ok``Err``Ok` 成员表示操作成功,内部包含成功时产生的值。`Err` 成员则意味着操作失败,并且包含失败的前因后果
`Result` 的成员是 `Ok``Err``Ok` 成员表示操作成功,内部包含成功时产生的值。`Err` 成员则意味着操作失败,并且 `Err` 中包含有关操作失败的原因或方式的信息
这些 `Result` 类型的作用是编码错误处理信息。`Result` 类型的值,像其他类型一样,拥有定义于其上的方法。`Result` 的实例拥有 [`expect` 方法][expect]<!-- ignore -->。如果 `io::Result` 实例的值是 `Err``expect` 会导致程序崩溃,并显示当做参数传递给 `expect` 的信息。如果 `read_line` 方法返回 `Err`,则可能是来源于底层操作系统错误的结果。如果 `Result` 实例的值是 `Ok``expect` 会获取 `Ok` 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。
@ -171,7 +169,7 @@ Rust 警告我们没有使用 `read_line` 的返回值 `Result`,说明有一
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print_guess}}
```
这行代码现在打印了存储用户输入的字符串。里面的 `{}` 是预留在特定位置的占位符:把 `{}` 想象成小蟹钳可以夹住合适的值。当打印变量的值时变量名可以写进大括号中。当打印表达式的执行结果时格式化字符串format string中大括号中留空格式化字符串后跟逗号分隔的需要打印的表达式列表其顺序与每一个空大括号占位符的顺序一致。在一个 `println!` 调用中打印变量和表达式的值看起来像这样:
这行代码现在打印了存储用户输入的字符串。`{}` 这对大括号是一个占位符:把 `{}` 想象成小蟹钳可以夹住合适的值。当打印变量的值时变量名可以写进大括号中。当打印表达式的执行结果时格式化字符串format string中大括号中留空格式化字符串后跟逗号分隔的需要打印的表达式列表其顺序与每一个空大括号占位符的顺序一致。在一个 `println!` 调用中打印变量和表达式的值看起来像这样:
```rust
let x = 5;
@ -184,7 +182,7 @@ println!("x = {x} and y + 2 = {}", y + 2);
### 测试第一部分代码
让我们来测试下猜猜看游戏的第一部分。使用 `cargo run` 运行:
让我们来测试下猜数字游戏的第一部分。使用 `cargo run` 运行:
```console
$ cargo run
@ -205,7 +203,7 @@ You guessed: 6
### 使用 crate 来增加更多功能
记住,*crate* 是一个 Rust 代码包。我们正在构建的项目是一个 *二进制 crate*,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,库 crate 可以包含任意能被其他程序使用的代码,但是不能自执行。
记住,crate 是一组 Rust 源代码文件。我们正在构建的项目是一个 *二进制 crate*,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,库 crate 可以包含任意能被其他程序使用的代码,但是无法独立执行。
Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 `rand` 编写代码之前,需要修改 *Cargo.toml* 文件,引入一个 `rand` 依赖。现在打开这个文件并将下面这一行添加到 `[dependencies]` 片段标题之下。在当前版本下,请确保按照我们这里的方式指定 `rand`,否则本教程中的示例代码可能无法工作。
@ -219,7 +217,9 @@ Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 `ra
Cargo 认为这些版本与 `0.8.5` 版本的公有 API 相兼容这样的版本指定确保了我们可以获取能使本章代码编译的最新的补丁patch版本。任何大于等于 `0.9.0` 的版本不能保证和接下来的示例采用了相同的 API。
现在,不修改任何代码,构建项目,如示例 2-2 所示:
现在,不修改任何代码,构建项目,如示例 2-2 所示。
<Listing number="2-2" caption="将 rand crate 添加为依赖之后运行 `cargo build` 的输出">
```console
$ cargo build
@ -242,15 +242,15 @@ $ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
```
<span class="caption">示例 2-2: 将 rand crate 添加为依赖之后运行 `cargo build` 的输出</span>
</Listing>
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),并且显示的行数可能会有所不同(取决于操作系统),行的顺序也可能会不同。
现在我们有了一个外部依赖Cargo 从 _registry_ 上获取所有包的最新版本信息,这是一份来自 [Crates.io][cratesio] 的数据拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方
现在我们有了一个外部依赖Cargo 从 _registry_ 上获取所有包的最新版本信息,这是一份来自 [Crates.io][cratesio] 的数据副本。Crates.io 是 Rust 生态系统中,人们发布其开源 Rust 项目的平台,供他人使用
在更新完 registry 后Cargo 检查 `[dependencies]` 片段并下载列表中包含但还未下载的 crates。本例中虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `rand` 所需要的其他 crates因为 `rand` 依赖它们来正常工作。下载完成后Rust 编译依赖,然后使用这些依赖编译项目。
在更新完 _registry_Cargo 检查 `[dependencies]` 片段并下载列表中包含但还未下载的 crates。本例中虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `rand` 所需要的其他 crates因为 `rand` 依赖它们来正常工作。下载完成后Rust 编译依赖,然后使用这些依赖编译项目。
如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 行之外的输出。Cargo 知道它已经下载并编译了依赖,同时 _Cargo.toml_ 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了
如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 行之外的输出。Cargo 知道它已经下载并编译了依赖,同时 _Cargo.toml_ 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它会简单地退出
如果打开 _src/main.rs_ 文件,做一些无关紧要的修改,保存并再次构建,则会出现两行输出:
@ -264,7 +264,7 @@ $ cargo build
#### _Cargo.lock_ 文件确保构建是可重现的
Cargo 有一个机制来确保任何人在任何时候重新构建代码都会产生相同的结果Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的。例如,如果下周 `rand` crate 的 `0.8.6` 版本出来了,它修复了一个重要的 bug同时也含有一个会破坏代码运行的缺陷。为了处理这个问题Rust 在你第一次运行 `cargo build` 时建*Cargo.lock* 文件,我们现在可以在*guessing_game* 目录找到它。
Cargo 有一个机制确保无论是你还是其他人在任何时候重新构建代码都会生成相同的构建产物Cargo 只会使用你指定的依赖版本,除非你明确指定其他版本。例如,如果下周 `rand` crate 的 `0.8.6` 版本出来了,该版本包含了一个重要的 bug 修复,但同时也引入了一个会破坏你代码的回归问题。为了解决这个问题Rust 在你第一次运行 `cargo build`建了 *Cargo.lock* 文件,我们现在可以在 *guessing_game* 目录找到它。
当第一次构建项目时Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时Cargo 会发现 *Cargo.lock* 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 `0.8.5` 直到你显式升级,多亏有了 *Cargo.lock* 文件。由于 *Cargo.lock* 文件对于“可重复构建”非常重要,因此它通常会和项目中的其余代码一样纳入到版本控制系统中。
@ -282,25 +282,24 @@ Cargo 忽略了 `0.9.0` 版本。这时,你也会注意到的 *Cargo.lock* 文
```toml
[dependencies]
rand = "0.9.0"
```
下一次运行 `cargo build`Cargo 会从 registry 更新可用的 crate并根据你指定的新版本重新计算
下一次运行 `cargo build`Cargo 会更新可用 crate 的 registry并根据你指定的新版本重新评估 `rand` 的要求
第十四章会讲到 [Cargo][doccargo]<!-- ignore --> 及其[生态系统][doccratesio]<!-- ignore --> 的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。
### 生成一个随机数
让我们开始使用 `rand` 来生成一个猜猜看随机数。下一步是更新 *src/main.rs*,如示例 2-3 所示。
让我们开始使用 `rand` 来生成一个猜数字随机数。下一步是更新 *src/main.rs*,如示例 2-3 所示。
<span class="filename">文件名src/main.rs</span>
<Listing number="2-3" file-name="src/main.rs" caption="添加生成随机数的代码">
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-03/src/main.rs:all}}
```
<span class="caption">示例 2-3添加生成随机数的代码</span>
</Listing>
首先,我们新增了一行 `use rand::Rng;`。`Rng` 是一个 trait它定义了随机数生成器应实现的方法想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。
@ -339,13 +338,13 @@ You guessed: 5
现在有了用户输入和一个随机数,我们可以比较它们。这个步骤如示例 2-4 所示。注意这段代码还不能通过编译,我们稍后会解释。
<span class="filename">文件名src/main.rs</span>
<Listing number="2-4" file-name="src/main.rs" caption="处理比较两个数字可能的返回值">
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-04/src/main.rs:here}}
```
<span class="caption">示例 2-4处理比较两个数字可能的返回值</span>
</Listing>
首先我们增加了另一个 `use` 声明,从标准库引入了一个叫做 `std::cmp::Ordering` 的类型到作用域中。 `Ordering` 也是一个枚举,不过它的成员是 `Less`、`Greater` 和 `Equal`。这是比较两个值时可能出现的三种结果。
@ -357,7 +356,7 @@ You guessed: 5
比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的代码并来到下一个分支。下一个分支的模式是 `Ordering::Greater`**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式会在第一次成功匹配后终止,因为该场景下没有检查最后一个分支的必要。
然而,示例 2-4 的代码并不能编译,可以尝试一下:
然而,示例 2-4 的代码目前并不能编译,可以尝试一下:
```console
{{#include ../listings/ch02-guessing-game-tutorial/listing-02-04/output.txt}}
@ -465,13 +464,13 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 `guess``String` 转化为 `u32` 那部分代码来实现,如示例 2-5 所示:
<span class="filename">文件名src/main.rs</span>
<Listing number="2-5" file-name="src/main.rs" caption="忽略非数字的猜测并重新请求数字而不是让程序崩溃">
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:here}}
```
<span class="caption">示例 2-5: 忽略非数字的猜测并重新请求数字而不是让程序崩溃</span>
</Listing>
我们将 `expect` 调用换成 `match` 语句,以从遇到错误就崩溃转换为处理错误。须知 `parse` 返回一个 `Result` 类型,而 `Result` 是一个拥有 `Ok``Err` 成员的枚举。这里使用的 `match` 表达式,和之前处理 `cmp` 方法返回 `Ordering` 时用的一样。
@ -479,7 +478,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
如果 `parse` **不**能将字符串转换为一个数字,它会返回一个包含更多错误信息的 `Err`。`Err` 值不能匹配第一个 `match` 分支的 `Ok(num)` 模式,但是会匹配第二个分支的 `Err(_)` 模式:`_` 是一个通配符值,本例中用来匹配所有 `Err` 值,不管其中有何种信息。所以程序会执行第二个分支的动作,`continue` 意味着进入 `loop` 的下一次循环,请求另一个猜测。这样程序就有效的忽略了 `parse` 可能遇到的所有错误!
现在万事俱备,只需运行 `cargo run`
现在程序中的一切都应该如预期般工作了。让我们试试吧
```console
$ cargo run
@ -504,17 +503,17 @@ You guessed: 61
You win!
```
太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`。示例 2-6 为最终代码:
太棒了!再有最后一个小的修改,就能完成猜数字游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`。示例 2-6 为最终代码:
<span class="filename">文件名src/main.rs</span>
<Listing number="2-6" file-name="src/main.rs" caption="猜数字游戏的完整代码">
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-06/src/main.rs}}
```
<span class="caption">示例 2-6猜猜看游戏的完整代码</span>
</Listing>
此时此刻,你顺利完成了猜猜看游戏。恭喜!
此时此刻,你顺利完成了猜数字游戏。恭喜!
## 总结