diff --git a/src/SUMMARY.md b/src/SUMMARY.md index c4e4026..a0b78a6 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -11,7 +11,7 @@ - [Hello, World!](ch01-02-hello-world.md) - [Hello, Cargo!](ch01-03-hello-cargo.md) -- [写个猜数字游戏](ch02-00-guessing-game-tutorial.md) +- [编写一个猜数字游戏](ch02-00-guessing-game-tutorial.md) - [常见编程概念](ch03-00-common-programming-concepts.md) - [变量与可变性](ch03-01-variables-and-mutability.md) diff --git a/src/ch01-03-hello-cargo.md b/src/ch01-03-hello-cargo.md index 646c65b..0317f45 100644 --- a/src/ch01-03-hello-cargo.md +++ b/src/ch01-03-hello-cargo.md @@ -1,12 +1,11 @@ ## Hello, Cargo! -> [ch01-03-hello-cargo.md](https://github.com/rust-lang/book/blob/main/src/ch01-03-hello-cargo.md) ->
-> commit 299fd1f3e11dd61ca136fb51d713f6b0ba7515ff + + Cargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。(我们把代码所需要的库叫做 **依赖**(*dependencies*))。 -最简单的 Rust 程序,比如我们刚刚编写的,没有任何依赖。如果使用 Cargo 来构建 “Hello, world!” 项目,将只会用到 Cargo 构建代码的那部分功能。在编写更复杂的 Rust 程序时,你将添加依赖项,如果使用 Cargo 启动项目,则添加依赖项将更容易。 +最简单的 Rust 程序,比如我们刚刚编写的,没有任何依赖。如果使用 Cargo 来构建 “Hello, world!” 项目,将只会用到 Cargo 构建代码的那部分功能。在编写更复杂的 Rust 程序时,你将添加依赖项,如果使用 Cargo 启动项目,则添加依赖项将更加容易。 由于绝大多数 Rust 项目使用 Cargo,本书接下来的部分假设你也使用 Cargo。如果使用 [“安装”][installation] 部分介绍的官方安装包的话,则自带了 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令检查是否安装了 Cargo: @@ -31,9 +30,9 @@ $ cd hello_cargo 这也会在 *hello_cargo* 目录初始化了一个 git 仓库,以及一个 *.gitignore* 文件。如果在一个已经存在的 git 仓库中运行 `cargo new`,则这些 git 相关文件则不会生成;可以通过运行 `cargo new --vcs=git` 来覆盖这些行为。 -> 注意:Git 是一个常用的版本控制系统(version control system,VCS)。可以通过 `--vcs` 参数使 `cargo new` 切换到其它版本控制系统(VCS),或者不使用 VCS。运行 `cargo new --help` 参看可用的选项。 +> 注意:git 是一个常用的版本控制系统(version control system,VCS)。可以通过 `--vcs` 参数使 `cargo new` 切换到其它版本控制系统(VCS),或者不使用 VCS。运行 `cargo new --help` 查看可用的选项。 -请自行选用文本编辑器打开 *Cargo.toml* 文件。它应该看起来如示例 1-2 所示: +请自行选用文本编辑器打开 *Cargo.toml* 文件。它应该看起来与示例 1-2 中代码类似:
@@ -43,24 +42,22 @@ $ cd hello_cargo [package] name = "hello_cargo" version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +edition = "2024" [dependencies] ``` -
示例 1-2:*cargo new* 命令生成的 *Cargo.toml* 的内容
+
示例 1-2:`cargo new` 命令生成的 *Cargo.toml* 的内容
这个文件使用 [*TOML*][toml] (*Tom's Obvious, Minimal Language*) 格式,这是 Cargo 配置文件的格式。 -第一行,`[package]`,是一个片段(section)标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他片段(section)。 +第一行,`[package]`,是一个片段 section 标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他 section。 接下来的三行设置了 Cargo 编译程序所需的配置:项目的名称、项目的版本以及要使用的 Rust 版本。[附录 E][appendix-e] 会介绍 `edition` 的值。 -最后一行,`[dependencies]`,是罗列项目依赖的片段的开始。在 Rust 中,代码包被称为 *crates*。这个项目并不需要其他的 crate,不过在第二章的第一个项目会用到依赖,那时会用得上这个片段。 +最后一行,`[dependencies]`,是罗列项目依赖的 section 的开始。在 Rust 中,代码包被称为 *crates*。这个项目并不需要其他的 crate,不过在第二章的第一个项目会用到依赖,那时会用得上这个 section。 现在打开 *src/main.rs* 看看: @@ -74,7 +71,7 @@ fn main() { Cargo 为你生成了一个 “Hello, world!” 程序,正如我们之前编写的示例 1-1!目前为止,我们的项目与 Cargo 生成项目的区别是 Cargo 将代码放在 *src* 目录,同时项目根目录包含一个 *Cargo.toml* 配置文件。 -Cargo 期望源文件存放在 *src* 目录中。项目根目录只存放 README、license 信息、配置文件和其他跟代码无关的文件。使用 Cargo 帮助你保持项目干净整洁,一切井井有条。 +Cargo 期望源文件存放在 *src* 目录中。项目根目录只存放 README、license 信息、配置文件和其他跟代码无关的文件。使用 Cargo 帮助你保持项目干净整洁。一切各得其所,井井有条。 如果没有使用 Cargo 开始项目,比如我们创建的 “Hello, world!” 项目,你可以将其转换为使用 Cargo 的项目。将项目代码移入 *src* 目录,并创建一个合适的 *Cargo.toml* 文件。一个简单的创建 *Cargo.toml* 文件的方法是运行 `cargo init`,它会自动为你创建该文件。 @@ -95,7 +92,7 @@ $ ./target/debug/hello_cargo # 或者在 Windows 下为 .\target\debug\hello_car Hello, world! ``` -如果一切顺利,终端上应该会打印出 `Hello, world!`。首次运行 `cargo build` 时,也会使 Cargo 在项目根目录创建一个新文件:*Cargo.lock*。这个文件记录项目依赖的实际版本。这个项目并没有依赖,所以其内容比较少。你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。 +如果一切顺利,终端上应该会打印出 `Hello, world!`。首次运行 `cargo build` 时,也会使 Cargo 在项目根目录创建一个新文件:*Cargo.lock*。这个文件记录项目依赖的实际版本。这个项目并没有依赖,所以其内容比较少。你永远也不需要手动编辑该文件;`Cargo` 会为你管理它。 我们刚刚使用 `cargo build` 构建了项目,并使用 `./target/debug/hello_cargo` 运行了程序,也可以使用 `cargo run` 在一个命令中同时编译并运行生成的可执行文件: @@ -106,9 +103,9 @@ $ cargo run Hello, world! ``` -比起要记得运行 `cargo build` 之后再用可执行文件的完整路径来运行程序,使用 `cargo run` 可以实现完全相同的效果,而且要方便得多,所以大多数开发者会使用 `cargo run`。 +比起要记得运行 `cargo build` 之后再用可执行文件的完整路径来运行程序,使用 `cargo run` 更方便,所以大多数开发者会使用 `cargo run`。 -注意这一次并没有出现表明 Cargo 正在编译 `hello_cargo` 的输出。Cargo 发现文件并没有被改变,所以它并没有重新编译,而是直接运行了可执行文件。如果修改了源文件的话,Cargo 会在运行之前重新构建项目,并会出现像这样的输出: +注意这一次并没有出现表明 Cargo 正在编译 `hello_cargo` 的输出。Cargo 发现文件并没有被改变,所以它并没有重新构建,而是直接运行了二进制文件。如果修改了源文件的话,Cargo 会在运行之前重新构建项目,并会出现像这样的输出: ```console $ cargo run @@ -136,15 +133,15 @@ $ cargo check * 可以使用 `cargo check` 在不生成二进制文件的情况下构建项目来检查错误。 * 有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 *target/debug* 目录。 -使用 Cargo 的一个额外的优点是,不管你使用什么操作系统,其命令都是一样的。所以从现在开始本书将不再为 Linux 和 macOS 以及 Windows 提供相应的命令。 +使用 Cargo 的一个额外的优点是,不论你使用什么操作系统,其命令都是一样的。所以从现在开始本书将不再分别为 Linux 和 macOS 以及 Windows 提供相应的命令。 ### 发布(release)构建 -当项目最终准备好发布时,可以使用 `cargo build --release` 来优化编译项目。这会在 *target/release* 而不是 *target/debug* 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:一种是为了开发,你需要经常快速重新构建;另一种是为用户构建最终程序,它们不会经常重新构建,并且希望程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 `cargo build --release` 并使用 *target/release* 下的可执行文件进行测试。 +当项目最终准备好发布时,可以使用 `cargo build --release` 来优化编译项目。这会在 *target/release* 而不是 *target/debug* 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:一种是为了开发,你需要快速且频繁地重新构建;另一种是为用户构建最终程序,它们不会经常重新构建,并且希望程序运行得越快越好。如果你在基准测试代码的运行时间,请确保运行 `cargo build --release` 并使用 *target/release* 下的可执行文件进行测试。 ### 把 Cargo 当作习惯 -对于简单项目,Cargo 并不比 `rustc` 提供了更多的优势,不过随着开发的深入,终将证明其价值。一旦程序壮大到由多个文件组成,亦或者是需要其他的依赖,让 Cargo 协调构建过程就会简单得多。 +对于简单项目,Cargo 并不比 `rustc` 提供了更多的优势,但随着程序变得更复杂,其价值会逐渐显现。一旦程序壮大到由多个文件组成,亦或者是需要其他的依赖,让 Cargo 协调构建过程就会简单得多。 即便 `hello_cargo` 项目十分简单,它现在也使用了很多在你之后的 Rust 生涯将会用到的实用工具。其实,要在任何已存在的项目上工作时,可以使用如下命令通过 Git 检出代码,移动到该项目目录并构建: @@ -166,7 +163,7 @@ $ cargo build * 直接通过 `rustc` 编写并运行 Hello, world! 程序 * 使用 Cargo 创建并运行新项目 -是时候通过构建更实质性的程序来熟悉读写 Rust 代码了。所以在第二章我们会构建一个猜猜看游戏程序。如果你更愿意从学习 Rust 常用的编程概念开始,请阅读第三章,接着再回到第二章。 +是时候通过构建更实质性的程序来熟悉读写 Rust 代码了。所以在第二章我们会构建一个猜数字游戏程序。如果你更愿意从学习 Rust 常用的编程概念开始,请阅读第三章,接着再回到第二章。 [installation]: ch01-01-installation.html#安装 [toml]: https://toml.io diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index e4536c3..ab0cd75 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -1,8 +1,7 @@ -# 写个猜数字游戏 +# 编写一个猜数字游戏 -> [ch02-00-guessing-game-tutorial.md](https://github.com/rust-lang/book/blob/main/src/ch02-00-guessing-game-tutorial.md) ->
-> commit 11ca3d508b0a28b03f7d9f16c88726088fafd87e + + 让我们一起动手完成一个项目来快速上手 Rust!本章将介绍一些 Rust 中常见的概念,并通过真实的程序来展示如何运用它们。你将会学到 `let`、`match`、方法(methods)、关联函数(associated functions)、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将主要练习基础内容。 @@ -61,7 +60,7 @@ $ cd guessing_game -这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io` 输入/输出库引入当前作用域。`io` 库来自于标准库,也被称为 `std`: +这些代码包含很多信息,我们一行一行地讲解。为了获取用户输入并打印结果作为输出,我们需要将 `io` 输入/输出库引入当前作用域。`io` 库来自于标准库,也被称为 `std`: ```rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:io}} @@ -101,7 +100,7 @@ $ cd guessing_game let apples = 5; ``` -这行代码新建了一个叫做 `apples` 的变量并把它绑定到值 `5` 上。在 Rust 中,变量默认是不可变的,这意味着一旦我们给变量赋值,这个值就不再可以修改了。我们将会在第三章的 [“变量与可变性”][variables-and-mutability] 部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 `mut` 来使一个变量可变: +这行代码新建了一个叫做 `apples` 的变量并把它绑定到值 `5` 上。在 Rust 中,变量默认是不可变的,这意味着一旦我们给变量赋值,这个值就不可以再修改了。我们将会在第三章的 [“变量与可变性”][variables-and-mutability] 部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 `mut` 来使一个变量可变: ```rust,ignore let apples = 5; // 不可变 @@ -114,7 +113,7 @@ let mut bananas = 5; // 可变 `::new` 那一行的 `::` 语法表明 `new` 是 `String` 类型的一个 **关联函数**(_associated function_)。关联函数是针对某个类型实现的函数,在这个例子中是 `String`。这个 `new` 函数创建了一个新的空字符串。你会发现许多类型上都有一个 `new` 函数,因为这是为某种类型创建新值的常用函数名。 -总的来说,`let mut guess = String::new();` 这一行创建了一个可变变量,当前它绑定到一个新的 `String` 空实例上。 +总的来说,`let mut guess = String::new();` 这一行创建了一个可变变量,当前它绑定到一个新的 `String` 空实例上。呼! ### 接收用户输入 @@ -128,7 +127,7 @@ let mut bananas = 5; // 可变 接下来,代码中的 `.read_line(&mut guess)` 调用了标准输入句柄上的 [`read_line`][read_line] 方法,以获取用户输入。我们还将 `&mut guess` 作为参数传递给 `read_line` 函数,让其将用户输入储存到这个字符串中。`read_line` 的工作是,无论用户在标准输入中键入什么内容,都将其追加(不会覆盖其原有内容)到一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 `read_line` 将用户输入附加上去。 -`&` 表示这个参数是一个 **引用**(_reference_),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 `&mut guess` 来使其可变,而不是 `&guess`。(第四章会更全面的解释引用。) +`&` 表示这个参数是一个 **引用**(_reference_),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 `&mut guess` 来使其可变,而不是 `&guess`。(第四章会更全面地讲解引用。) ### 使用 `Result` 类型来处理潜在的错误 @@ -146,14 +145,13 @@ io::stdin().read_line(&mut guess).expect("Failed to read line"); 不过,过长的代码行难以阅读,所以最好拆开来写。通常来说,当使用 `.method_name()` 语法调用方法时引入换行符和空格将长的代码行拆开是明智的。现在来看看这行代码干了什么。 -之前提到了 `read_line` 会将用户输入附加到传递给它的字符串中,不过它也会返回一个类型为 `Result` 的值。 -[`Result`][result] 是一种[*枚举类型*][enums],通常也写作 *enum*。枚举类型变量的值可以是多种可能状态中的一个。我们把每种可能的状态称为一种 *枚举成员(variant)*。 +之前提到了 `read_line` 会将用户输入附加到传递给它的字符串中,不过它也会返回一个类型为 `Result` 的值。[`Result`][result] 是一种[*枚举类型*][enums],通常也写作 *enum*,它可以是多种可能状态中的一个。我们把每种可能的状态称为一种 **枚举成员**(*variant*)。 [第六章][enums]将介绍枚举的更多细节。这里的 `Result` 类型将用来编码错误处理的信息。 `Result` 的成员是 `Ok` 和 `Err`,`Ok` 成员表示操作成功,内部包含成功时产生的值。`Err` 成员则意味着操作失败,并且 `Err` 中包含有关操作失败的原因或方式的信息。 -这些 `Result` 类型的作用是编码错误处理信息。`Result` 类型的值,像其他类型一样,拥有定义于其实例上的方法。`Result` 的实例拥有 [`expect` 方法][expect]。如果 `io::Result` 实例的值是 `Err`,`expect` 会导致程序崩溃,并输出当做参数传递给 `expect` 的信息。所以当 `read_line` 方法返回 `Err`,则可能是来源于底层操作系统错误的结果。如果 `Result` 实例的值是 `Ok`,`expect` 会获取 `Ok` 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。 +`Result` 类型的值,像其他类型一样,拥有定义于其实例上的方法。`Result` 的实例拥有 [`expect` 方法][expect]。如果 `io::Result` 实例的值是 `Err`,`expect` 会导致程序崩溃,并输出当做参数传递给 `expect` 的信息。所以当 `read_line` 方法返回 `Err`,则可能是来源于底层操作系统错误的结果。如果 `Result` 实例的值是 `Ok`,`expect` 会获取 `Ok` 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。 如果不调用 `expect`,程序也能编译,不过会出现一个警告: @@ -167,7 +165,7 @@ Rust 警告我们没有使用 `read_line` 的返回值 `Result`,说明有一 ### 使用 `println!` 占位符打印值 -除了位于结尾的右花括号,目前为止就只有这一行代码值得讨论一下了,就是这一行: +除了位于结尾的右花括号,目前为止就只有这一行代码值得讨论一下了: ```rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print_guess}} @@ -191,7 +189,7 @@ println!("x = {x} and y + 2 = {}", y + 2); ```console $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 6.44s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s Running `target/debug/guessing_game` Guess the number! Please input your guess. @@ -209,7 +207,7 @@ You guessed: 6 记住,crate 是一组 Rust 源代码文件。我们正在构建的项目是一个 *二进制 crate*,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,库 crate 可以包含任意能被其他程序使用的代码,但是无法独立执行。 -Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 `rand` 编写代码之前,需要修改 *Cargo.toml* 文件,引入一个 `rand` 依赖。现在打开这个文件并将下面这一行添加到 `[dependencies]` 片段标题之下。在当前版本下,请确保按照我们这里的方式指定 `rand`,否则本教程中的示例代码可能无法工作。 +Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 `rand` 编写代码之前,需要修改 *Cargo.toml* 文件,引入一个 `rand` 依赖。现在打开这个文件并将下面这一行添加到 `[dependencies]` section 标题之下。在当前版本下,请确保按照我们这里的方式指定 `rand`,否则本教程中的示例代码可能无法工作。 文件名:Cargo.toml @@ -217,7 +215,7 @@ Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 `ra {{#include ../listings/ch02-guessing-game-tutorial/listing-02-02/Cargo.toml:8:}} ``` -在 _Cargo.toml_ 文件中,标题以及之后的内容属同一个片段,直到遇到下一个标题才开始新的片段。`[dependencies]` 片段告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 `0.8.5` 来指定 `rand` crate。Cargo 理解 [语义化版本(Semantic Versioning)][semver](有时也称为 _SemVer_),这是一种定义版本号的标准。`0.8.5` 事实上是 `^0.8.5` 的简写,它表示任何至少是 `0.8.5` 但小于 `0.9.0` 的版本。 +在 _Cargo.toml_ 文件中,标题以及之后的内容属同一个 section,直到遇到下一个标题才开始新的 section。`[dependencies]` section 告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 `0.8.5` 来指定 `rand` crate。Cargo 理解 [语义化版本(Semantic Versioning)][semver](有时也称为 _SemVer_),这是一种定义版本号的标准。`0.8.5` 事实上是 `^0.8.5` 的简写,它表示任何至少是 `0.8.5` 但小于 `0.9.0` 的版本。 Cargo 认为这些版本与 `0.8.5` 版本的公有 API 相兼容,这样的版本指定确保了我们可以获取能使本章代码编译的最新的补丁(patch)版本。任何大于等于 `0.9.0` 的版本不能保证和接下来的示例采用了相同的 API。 @@ -227,23 +225,25 @@ Cargo 认为这些版本与 `0.8.5` 版本的公有 API 相兼容,这样的版 ```console $ 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 + Updating crates.io index + Locking 15 packages to latest Rust 1.85.0 compatible versions + Adding rand v0.8.5 (available: v0.9.0) + Compiling proc-macro2 v1.0.93 + Compiling unicode-ident v1.0.17 + Compiling libc v0.2.170 + Compiling cfg-if v1.0.0 + Compiling byteorder v1.5.0 + Compiling getrandom v0.2.15 + Compiling rand_core v0.6.4 + Compiling quote v1.0.38 + Compiling syn v2.0.98 + Compiling zerocopy-derive v0.7.35 + Compiling zerocopy v0.7.35 + Compiling ppv-lite86 v0.2.20 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling guessing_game v0.1.0 (file:///projects/guessing_game) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s ```
示例 2-2:将 rand crate 添加为依赖之后运行 `cargo build` 的输出
@@ -254,25 +254,25 @@ $ cargo build 现在我们有了一个外部依赖,Cargo 从 _registry_ 上获取所有包的最新版本信息,这是一份来自 [Crates.io][cratesio] 的数据副本。Crates.io 是 Rust 生态系统中,人们发布其开源 Rust 项目的平台,供他人使用。 -在更新完 _registry_ 后,Cargo 检查 `[dependencies]` 片段并下载列表中包含但还未下载的 crates。本例中,虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `rand` 所需要的其他 crates,因为 `rand` 依赖它们来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。 +在更新完 _registry_ 后,Cargo 检查 `[dependencies]` section 并下载列表中包含但还未下载的 crate。本例中,虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `rand` 所需要的其他 crate,因为 `rand` 依赖它们来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。 如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 行之外的输出。Cargo 知道它已经下载并编译了依赖,同时 _Cargo.toml_ 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它会简单地退出。 -如果打开 _src/main.rs_ 文件,做一些无关紧要的修改,保存并再次构建,则会出现两行输出: +如果打开 _src/main.rs_ 文件,做一些无关紧要的修改,保存并再次构建,你将只会看到两行输出: ```console $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s ``` -这一行表示 Cargo 只针对 _src/main.rs_ 文件的微小修改而更新构建。依赖没有变化,所以 Cargo 知道它可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。 +这一行表示 Cargo 只针对 _src/main.rs_ 文件的微小修改而更新构建。依赖没有变化,所以 Cargo 知道它可以复用已经为此下载并编译的代码。 -#### _Cargo.lock_ 文件确保构建是可重现的 +#### _Cargo.lock_ 文件确保可重现构建 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* 文件对于“可重复构建”非常重要,因此它通常会和项目中的其余代码一样纳入到版本控制系统中。 +当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时,Cargo 会发现 *Cargo.lock* 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现构建(reproducible build)。换句话说,项目会持续使用 `0.8.5` 直到你显式升级,多亏有了 *Cargo.lock* 文件。由于 *Cargo.lock* 文件对于可重现构建非常重要,因此它通常会和项目中的其余代码一样提交到版本控制系统中。 #### 更新 crate 到一个新版本 @@ -281,10 +281,11 @@ Cargo 有一个机制,确保无论是你还是其他人在任何时候重新 ```console $ cargo update Updating crates.io index - Updating rand v0.8.5 -> v0.8.6 + Locking 1 package to latest Rust 1.85.0 compatible version + Updating rand v0.8.5 -> v0.8.6 (available: v0.9.0) ``` -Cargo 忽略了 `0.9.0` 版本。这时,你也会注意到的 *Cargo.lock* 文件中的变化无外乎现在使用的 `rand` crate 版本是`0.8.6` 。如果想要使用 `0.9.0` 版本的 `rand` 或是任何 `0.9.x` 系列的版本,必须像这样更新 *Cargo.toml* 文件: +Cargo 忽略了 `0.9.0` 版本。这时,你也会注意到的 *Cargo.lock* 文件中的变化无外乎现在使用的 `rand` crate 版本是 `0.8.6` 。如果想要使用 `0.9.0` 版本的 `rand` 或是任何 `0.9.x` 系列的版本,必须像这样更新 *Cargo.toml* 文件: ```toml [dependencies] @@ -297,7 +298,7 @@ rand = "0.9.0" ### 生成一个随机数 -让我们开始使用 `rand` 来生成一个猜数字随机数。下一步是更新 *src/main.rs*,如示例 2-3 所示。 +让我们开始使用 `rand` 来生成一个要猜测的数字。下一步是更新 *src/main.rs*,如示例 2-3 所示。
@@ -313,9 +314,9 @@ rand = "0.9.0" 首先,我们新增了一行 `use rand::Rng;`。`Rng` 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。第十章会详细介绍 trait。 -接下来,我们在中间还新增加了两行。第一行调用了 `rand::thread_rng` 函数提供实际使用的随机数生成器:它位于当前执行线程的本地环境中,并从操作系统获取 seed。接着调用随机数生成器的 `gen_range` 方法。这个方法由 `use rand::Rng` 语句引入到作用域的 `Rng` trait 定义。`gen_range` 方法获取一个范围表达式(range expression)作为参数,并生成一个在此范围之间的随机数。这里使用的这类范围表达式使用了 `start..=end` 这样的形式,也就是说包含了上下端点,所以需要指定 `1..=100` 来请求一个 1 和 100 之间的数。 +接下来,我们在中间还新增加了两行。第一行调用了 `rand::thread_rng` 函数提供实际使用的随机数生成器:它位于当前执行线程的本地环境中,并从操作系统获取 seed。接着调用随机数生成器的 `gen_range` 方法。这个方法由 `use rand::Rng` 语句引入到作用域的 `Rng` trait 定义。`gen_range` 方法获取一个范围表达式(range expression)作为参数,并生成一个在此范围之间的随机数。这里使用的这类范围表达式使用了 `start..=end` 这样的形式,它对上下边界均为闭区间,所以需要指定 `1..=100` 来请求一个 1 和 100 之间的数。 -> 注意:你不可能凭空就知道应该 use 哪个 trait 以及该从 crate 中调用哪个方法,因此每个 crate 有使用说明文档。Cargo 有一个很棒的功能是:运行 `cargo doc --open` 命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 `rand` crate 中的其他功能感兴趣,你可以运行 `cargo doc --open` 并点击左侧导航栏中的 `rand`。 +> 注意:你不可能凭空就知道应该 use 哪个 trait 以及该从 crate 中调用哪个方法,因此每个 crate 有使用说明文档。Cargo 的另一个很棒的功能是运行 `cargo doc --open` 命令来构建所有本地依赖提供的文档并在浏览器中打开。例如,假设你对 `rand` crate 中的其他功能感兴趣,你可以运行 `cargo doc --open` 并点击左侧导航栏中的 `rand`。 新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为可以测试它,不过在最终版本中会删掉它。如果游戏一开始就打印出结果就没什么可玩的了! @@ -324,7 +325,7 @@ rand = "0.9.0" ```console $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 2.53s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/guessing_game` Guess the number! The secret number is: 7 @@ -333,7 +334,7 @@ Please input your guess. You guessed: 4 $ cargo run - Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/guessing_game` Guess the number! The secret number is: 83 @@ -364,11 +365,11 @@ You guessed: 5 接着,底部的五行新代码使用了 `Ordering` 类型,`cmp` 方法用来比较两个值并可以在任何可比较的值上调用。它获取一个被比较值的引用:这里是把 `guess` 与 `secret_number` 做比较。然后它会返回一个刚才通过 `use` 引入作用域的 `Ordering` 枚举的成员。使用一个 [`match`][match] 表达式,根据对 `guess` 和 `secret_number` 调用 `cmp` 返回的 `Ordering` 成员来决定接下来做什么。 -一个 `match` 表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和表达式开头的值与分支模式相匹配时应该执行的代码。Rust 获取提供给 `match` 的值并挨个检查每个分支的模式。`match` 结构和模式是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并帮助你确保没有遗漏处理。这些功能将分别在第六章和第十九章详细介绍。 +一个 `match` 表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和表达式开头的值与分支模式相匹配时应该执行的代码。Rust 获取提供给 `match` 的值并挨个检查每个分支的模式。`match` 结构和模式是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并确保对所有情况作出处理。这些功能将分别在第六章和第十九章详细介绍。 让我们看看使用 `match` 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。 -比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的代码并来到下一个分支。下一个分支的模式是 `Ordering::Greater`,**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式会在第一次成功匹配后终止,因为该场景下没有检查最后一个分支的必要。 +比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的代码并来到下一个分支。下一个分支的模式是 `Ordering::Greater`,**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式会在第一次成功匹配后终止,因此在这种情况下不会查看最后一个分支。 然而,示例 2-4 的代码目前并不能编译,可以尝试一下: @@ -378,7 +379,7 @@ You guessed: 5 错误的核心表明这里有 **不匹配的类型**(_mismatched types_)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 `let guess = String::new()` 时,Rust 推断出 `guess` 应该是 `String` 类型,并不需要我们写出类型。另一方面,`secret_number`,是数字类型。几个数字类型拥有 1 到 100 之间的值:32 位数字 `i32`;32 位无符号数字 `u32`;64 位数字 `i64` 等等。Rust 默认使用 `i32`,所以它是 `secret_number` 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。 -所以我们必须把从输入中读取到的 `String` 转换为一个真正的数字类型,才好与秘密数字进行比较。这可以通过在 `main` 函数体中增加如下代码来实现: +所以我们必须把从输入中读取到的 `String` 转换为一个数字类型,才好与秘密数字进行比较。这可以通过在 `main` 函数体中增加如下代码来实现: 文件名:src/main.rs @@ -392,9 +393,9 @@ You guessed: 5 let guess: u32 = guess.trim().parse().expect("Please type a number!"); ``` -这里创建了一个叫做 `guess` 的变量。不过等等,不是已经有了一个叫做 `guess` 的变量了吗?确实如此,不过 Rust 允许用一个新值来 **隐藏** (_Shadowing_) `guess` 之前的值。这个功能常用在需要转换值类型之类的场景。它允许我们复用 `guess` 变量的名字,而不是被迫创建两个不同变量,诸如 `guess_str` 和 `guess` 之类。[第三章][shadowing]会介绍 shadowing 的更多细节,现在只需知道这个功能经常用于将一个类型的值转换为另一个类型的值。 +这里创建了一个叫做 `guess` 的变量。不过等等,不是已经有了一个叫做 `guess` 的变量了吗?确实如此,不过 Rust 允许用一个新值来 **隐藏** (_Shadowing_) `guess` 之前的值。这个功允许我们复用 `guess` 变量的名字,而不是被迫创建两个不同变量,诸如 `guess_str` 和 `guess` 之类。[第三章][shadowing]会介绍 shadowing 的更多细节,现在只需知道这个功能经常用于将一个类型的值转换为另一个类型的值。 -我们将这个新变量绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 指的是包含输入的字符串类型 `guess` 变量。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符,我们必须执行此方法才能将字符串与 `u32` 比较,因为 `u32` 只能包含数值型数据。用户必须输入 enter 键才能让 `read_line` 返回并输入他们的猜想,这将会在字符串中增加一个换行(newline)符。例如,用户输入 5 并按下 enter(在 Windows 上,按下 enter 键会得到一个回车符和一个换行符,`\r\n`),`guess` 看起来像这样:`5\n` 或者 `5\r\n`。`\n` 代表 “换行”,回车键;`\r` 代表 “回车”,回车键。`trim` 方法会消除 `\n` 或者 `\r\n`,只留下 `5`。 +我们将这个新变量绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 指的是包含输入的字符串类型 `guess` 变量。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符,我们必须执行此方法才能将字符串与 `u32` 比较,因为 `u32` 只能包含数值型数据。用户必须输入 enter 键才能让 `read_line` 返回并输入他们的猜想,这将会在字符串中增加一个换行(newline)符。例如,用户输入 5 并按下 enter(在 Windows 上,按下 enter 键会得到一个回车符和一个换行符,`\r\n`),`guess` 看起来像这样:`5\n` 或者 `5\r\n`。`\n` 代表 “换行”,回车键;`\r` 代表 “回车”,回车键。`trim` 方法会消除 `\n` 或者 `\r\n`,结果只留下 `5`。 [字符串的 `parse` 方法][parse] 将字符串转换成其他类型。这里用它来把字符串转换为数值。我们需要告诉 Rust 具体的数字类型,这里通过 `let guess: u32` 指定。`guess` 后面的冒号(`:`)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;`u32` 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的默认类型,[第三章][integers]还会讲到其他数字类型。 @@ -407,7 +408,7 @@ let guess: u32 = guess.trim().parse().expect("Please type a number!"); ```console $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 0.43s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s Running `target/debug/guessing_game` Guess the number! The secret number is: 58 @@ -431,14 +432,14 @@ Too big! {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-04-looping/src/main.rs:here}} ``` -如上所示,我们将提示用户猜测之后的所有内容移动到了循环中。确保 loop 循环中的代码多缩进四个空格,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户好像无法退出啊! +如上所示,我们将提示用户猜测之后的所有内容移动到了循环中。确保 loop 循环中的代码多缩进四个空格,再次运行程序。注意这里有一个新问题,程序现在会不断地要求用户输入新的猜测。用户好像无法退出啊! -用户总能使用 ctrl-c 终止程序。不过还有另一个方法跳出无限循环,就是 [“比较猜测与秘密数字”](#比较猜测的数字和秘密数字) 部分提到的 `parse`:如果用户输入的答案不是一个数字,程序会崩溃。我们可以利用这一点来退出,如下所示: +用户总能使用 ctrl-c 终止程序。不过还有另一个方法跳出无限循环,就是 [“比较猜测与秘密数字”](#比较猜测的数字和秘密数字) 部分提到的 `parse`:如果用户输入的答案不是一个数字,程序会崩溃。我们可以利用这一点来退出,如下所示: ```console $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 1.50s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s Running `target/debug/guessing_game` Guess the number! The secret number is: 59 @@ -456,11 +457,13 @@ 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 + +thread 'main' panicked at src/main.rs:28:47: +Please type a number!: ParseIntError { kind: InvalidDigit } note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` -输入 `quit` 将会退出程序,同时你会注意到其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏停止。 +输入 `quit` 将会退出程序,同时你会注意到其他任何非数字输入也一样。这至少可以说是不理想的,我们想要当猜测正确的数字时游戏停止。 ### 猜测正确后退出 @@ -492,7 +495,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 我们将 `expect` 调用换成 `match` 语句,以从遇到错误就崩溃转换为处理错误。须知 `parse` 返回一个 `Result` 类型,而 `Result` 是一个拥有 `Ok` 或 `Err` 成员的枚举。这里使用的 `match` 表达式,和之前处理 `cmp` 方法返回 `Ordering` 时用的一样。 -如果 `parse` 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 `Ok`。这个 `Ok` 值与 `match` 第一个分支的模式相匹配,该分支对应的动作返回 `Ok` 值中的数字 `num`,最后如愿变成新创建的 `guess` 变量。 +如果 `parse` 能够成功地将字符串转换为一个数字,它会返回一个包含结果数字的 `Ok`。这个 `Ok` 值与 `match` 第一个分支的模式相匹配,该分支对应的动作返回 `Ok` 值中的数字 `num`,最后如愿变成新创建的 `guess` 变量。 如果 `parse` **不**能将字符串转换为一个数字,它会返回一个包含更多错误信息的 `Err`。`Err` 值不能匹配第一个 `match` 分支的 `Ok(num)` 模式,但是会匹配第二个分支的 `Err(_)` 模式:`_` 是一个通配符值,本例中用来匹配所有 `Err` 值,不管其中有何种信息。所以程序会执行第二个分支的动作,`continue` 意味着进入 `loop` 的下一次循环,请求另一个猜测。这样程序就有效的忽略了 `parse` 可能遇到的所有错误! @@ -501,7 +504,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ```console $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) - Finished dev [unoptimized + debuginfo] target(s) in 4.45s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s Running `target/debug/guessing_game` Guess the number! The secret number is: 61 @@ -521,7 +524,7 @@ You guessed: 61 You win! ``` -太棒了!再有最后一个小的修改,就能完成猜数字游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`。示例 2-6 为最终代码: +太棒了!再有最后一个小的修改,就能完成猜数字游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏体验。删掉打印秘密数字的 `println!`。示例 2-6 为最终代码:
@@ -539,7 +542,7 @@ You win! ## 总结 -本项目通过动手实践,向你介绍了 Rust 新概念:`let`、`match`、函数、使用外部 crate 等等,接下来的几章,你会继续深入学习这些概念。第三章介绍大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用它们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。 +本项目通过动手实践,向你介绍了 Rust 新概念:`let`、`match`、函数、使用外部 crate 等等,接下来的几章,你会继续深入学习这些概念。第三章介绍大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用它们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的特性。第五章讨论结构体和方法的语法,而第六章解释枚举。 [prelude]: https://doc.rust-lang.org/std/prelude/index.html [variables-and-mutability]: ch03-01-variables-and-mutability.html#变量和可变性