From 3308a21cfe88eadb3c21ed7eaf39b2cb41189068 Mon Sep 17 00:00:00 2001 From: Aaran Xu Date: Fri, 14 Jan 2022 23:46:03 +0800 Subject: [PATCH] Update Chapter Numbers --- src/appendix-03-derivable-traits.md | 8 +++---- src/appendix-05-editions.md | 2 +- src/ch00-00-introduction.md | 18 +++++++-------- src/ch02-00-guessing-game-tutorial.md | 12 +++++----- src/ch03-05-control-flow.md | 6 ++--- src/ch04-01-what-is-ownership.md | 12 +++++----- src/ch04-02-references-and-borrowing.md | 4 ++-- src/ch04-03-slices.md | 8 +++---- src/ch05-00-structs.md | 2 +- src/ch05-01-defining-structs.md | 10 ++++----- src/ch05-02-example-structs.md | 6 ++--- src/ch05-03-method-syntax.md | 6 ++--- src/ch06-01-defining-an-enum.md | 6 ++--- src/ch06-02-match.md | 2 +- ...ojects-with-packages-crates-and-modules.md | 2 +- src/ch07-01-packages-and-crates.md | 4 ++-- ...g-paths-into-scope-with-the-use-keyword.md | 8 +++---- src/ch08-01-vectors.md | 14 ++++++------ src/ch08-02-strings.md | 8 +++---- src/ch08-03-hash-maps.md | 6 ++--- src/ch09-02-recoverable-errors-with-result.md | 12 +++++----- src/ch09-03-to-panic-or-not-to-panic.md | 4 ++-- src/ch10-00-generics.md | 2 +- src/ch10-01-syntax.md | 6 ++--- src/ch10-02-traits.md | 6 ++--- src/ch10-03-lifetime-syntax.md | 8 +++---- src/ch11-01-writing-tests.md | 20 ++++++++--------- src/ch11-03-test-organization.md | 4 ++-- src/ch12-00-an-io-project.md | 12 +++++----- ...h12-01-accepting-command-line-arguments.md | 4 ++-- ...improving-error-handling-and-modularity.md | 12 +++++----- ...2-04-testing-the-librarys-functionality.md | 10 ++++----- src/ch13-00-functional-features.md | 2 +- src/ch13-01-closures.md | 6 ++--- src/ch13-02-iterators.md | 8 +++---- src/ch13-03-improving-our-io-project.md | 10 ++++----- src/ch14-02-publishing-to-crates-io.md | 6 ++--- src/ch14-03-cargo-workspaces.md | 2 +- src/ch14-04-installing-binaries.md | 2 +- src/ch15-00-smart-pointers.md | 4 ++-- src/ch15-01-box.md | 12 +++++----- src/ch15-02-deref.md | 4 ++-- src/ch15-04-rc.md | 4 ++-- src/ch15-05-interior-mutability.md | 8 +++---- src/ch16-01-threads.md | 8 +++---- src/ch16-02-message-passing.md | 4 ++-- src/ch16-03-shared-state.md | 10 ++++----- ...04-extensible-concurrency-sync-and-send.md | 8 +++---- src/ch17-01-what-is-oo.md | 4 ++-- src/ch17-02-trait-objects.md | 8 +++---- src/ch17-03-oo-design-patterns.md | 6 ++--- src/ch18-00-patterns.md | 2 +- src/ch18-01-all-the-places-for-patterns.md | 8 +++---- src/ch18-03-pattern-syntax.md | 6 ++--- src/ch19-00-advanced-features.md | 2 +- src/ch19-01-unsafe-rust.md | 10 ++++----- src/ch19-03-advanced-traits.md | 10 ++++----- src/ch19-04-advanced-types.md | 8 +++---- ...ch19-05-advanced-functions-and-closures.md | 2 +- src/ch19-06-macros.md | 6 ++--- src/ch20-01-single-threaded.md | 4 ++-- src/ch20-02-multithreaded.md | 22 +++++++++---------- src/ch20-03-graceful-shutdown-and-cleanup.md | 2 +- 63 files changed, 221 insertions(+), 221 deletions(-) diff --git a/src/appendix-03-derivable-traits.md b/src/appendix-03-derivable-traits.md index 0409e5a..5b6e3de 100644 --- a/src/appendix-03-derivable-traits.md +++ b/src/appendix-03-derivable-traits.md @@ -16,7 +16,7 @@ 一个无法被派生的 trait 的例子是为终端用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为终端用户显示一个类型。终端用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最相关的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。 -本附录所提供的可派生 trait 列表并不全面:库可以为其自己的 trait 实现 `derive`,可以使用 `derive` 的 trait 列表事实上是无限的。实现 `derive` 涉及到过程宏的应用,这在第十九章的 [“宏”][macros] 有介绍。 +本附录所提供的可派生 trait 列表并不全面:库可以为其自己的 trait 实现 `derive`,可以使用 `derive` 的 trait 列表事实上是无限的。实现 `derive` 涉及到过程宏的应用,这在第 19 章的 [“宏”][macros] 有介绍。 ### 用于开发者输出的 `Debug` @@ -54,13 +54,13 @@ ### 复制值的 `Clone` 和 `Copy` -`Clone` trait 可以明确地创建一个值的深拷贝(deep copy),复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第四章 [“变量和数据的交互方式:移动”][ways-variables-and-data-interact-clone] 以获取有关 `Clone` 的更多信息。 +`Clone` trait 可以明确地创建一个值的深拷贝(deep copy),复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第 4 章 [“变量和数据的交互方式:移动”][ways-variables-and-data-interact-clone] 以获取有关 `Clone` 的更多信息。 派生 `Clone` 实现了 `clone` 方法,其为整个的类型实现时,在类型的每一部分上调用了 `clone` 方法。这意味着类型中所有字段或值也必须实现了 `Clone`,这样才能够派生 `Clone` 。 例如,当在一个切片(slice)上调用 `to_vec` 方法时,`Clone` 是必须的。切片并不拥有其所包含实例的类型,但是从 `to_vec` 中返回的 vector 需要拥有其实例,因此,`to_vec` 在每个元素上调用 `clone`。因此,存储在切片中的类型必须实现 `Clone`。 -`Copy` trait 允许你通过只拷贝存储在栈上的位来复制值而不需要额外的代码。查阅第四章 [“只在栈上的数据:拷贝”][stack-only-data-copy] 的部分来获取有关 `Copy` 的更多信息。 +`Copy` trait 允许你通过只拷贝存储在栈上的位来复制值而不需要额外的代码。查阅第 4 章 [“只在栈上的数据:拷贝”][stack-only-data-copy] 的部分来获取有关 `Copy` 的更多信息。 `Copy` trait 并未定义任何方法来阻止编程人员重写这些方法或违反不需要执行额外代码的假设。尽管如此,所有的编程人员可以假设复制(copy)一个值非常快。 @@ -80,7 +80,7 @@ `Default` trait 使你创建一个类型的默认值。 派生 `Default` 实现了 `default` 函数。`default` 函数的派生实现调用了类型每部分的 `default` 函数,这意味着类型中所有的字段或值也必须实现了 `Default`,这样才能够派生 `Default` 。 -`Default::default` 函数通常结合结构体更新语法一起使用,这在第五章的 [“使用结构体更新语法从其他实例中创建实例”][creating-instances-from-other-instances-with-struct-update-syntax] 部分有讨论。可以自定义一个结构体的一小部分字段而剩余字段则使用 `..Default::default()` 设置为默认值。 +`Default::default` 函数通常结合结构体更新语法一起使用,这在第 5 章的 [“使用结构体更新语法从其他实例中创建实例”][creating-instances-from-other-instances-with-struct-update-syntax] 部分有讨论。可以自定义一个结构体的一小部分字段而剩余字段则使用 `..Default::default()` 设置为默认值。 例如,当你在 `Option` 实例上使用 `unwrap_or_default` 方法时,`Default` trait是必须的。如果 `Option` 是 `None`的话, `unwrap_or_default` 方法将返回存储在 `Option` 中 `T` 类型的 `Default::default` 的结果。 diff --git a/src/appendix-05-editions.md b/src/appendix-05-editions.md index c6fc959..596c014 100644 --- a/src/appendix-05-editions.md +++ b/src/appendix-05-editions.md @@ -1,6 +1,6 @@ ## 附录 E:版本 -早在第一章,我们见过 `cargo new` 在 *Cargo.toml* 中增加了一些有关 `edition` 的元数据。本附录将解释其意义! +早在第 1 章,我们见过 `cargo new` 在 *Cargo.toml* 中增加了一些有关 `edition` 的元数据。本附录将解释其意义! Rust 语言和编译器有一个为期 6 周的发布循环。这意味着用户会稳定得到新功能的更新。其他编程语言发布大更新但不甚频繁;Rust 选择更为频繁的发布小更新。一段时间之后,所有这些小更新会日积月累。不过随着小更新逐次的发布,或许很难回过头来感叹:“哇,从 Rust 1.10 到 Rust 1.31,Rust 的变化真大!” diff --git a/src/ch00-00-introduction.md b/src/ch00-00-introduction.md index 42bf6bc..67dca7a 100644 --- a/src/ch00-00-introduction.md +++ b/src/ch00-00-introduction.md @@ -49,23 +49,23 @@ Rust 语言也希望能支持很多其他用户,这里提及的只是最大的 总体来说,本书假设你会从头到尾顺序阅读。稍后的章节建立在之前章节概念的基础上,同时之前的章节可能不会深入讨论某个主题的细节;通常稍后的章节会重新讨论这些主题。 -你会在本书中发现两类章节:概念章节和项目章节。在概念章节中,我们学习 Rust 的某个方面。在项目章节中,我们应用目前所学的知识一同构建小的程序。第二、十二和二十章是项目章节;其余都是概念章节。 +你会在本书中发现两类章节:概念章节和项目章节。在概念章节中,我们学习 Rust 的某个方面。在项目章节中,我们应用目前所学的知识一同构建小的程序。第 2、12 和 20 章是项目章节;其余都是概念章节。 -第一章介绍如何安装 Rust,如何编写 “Hello, world!” 程序,以及如何使用 Rust 的包管理器和构建工具 Cargo。第二章是 Rust 语言的实战介绍。我们会站在较高的层次介绍一些概念,而将详细的介绍放在稍后的章节中。如果你希望立刻就动手实践一下,第二章正好适合你。开始阅读时,你甚至可能希望略过第三章,它介绍了 Rust 中类似其他编程语言中的功能,并直接阅读第四章学习 Rust 的所有权系统。然而,如果你是特别重视细节的学习者,并倾向于在继续之前学习每一个细节,你可能希望略过第二章并直接阅读第三章,并在想要构建项目来实践这些细节时再回来阅读第二章。 +第 1 章介绍如何安装 Rust,如何编写 “Hello, world!” 程序,以及如何使用 Rust 的包管理器和构建工具 Cargo。第 2 章是 Rust 语言的实战介绍。我们会站在较高的层次介绍一些概念,而将详细的介绍放在稍后的章节中。如果你希望立刻就动手实践一下,第 2 章正好适合你。开始阅读时,你甚至可能希望略过第 3 章,它介绍了 Rust 中类似其他编程语言中的功能,并直接阅读第 4 章学习 Rust 的所有权系统。然而,如果你是特别重视细节的学习者,并倾向于在继续之前学习每一个细节,你可能希望略过第 2 章并直接阅读第 3 章,并在想要构建项目来实践这些细节时再回来阅读第 2 章。 -第五章讨论结构体和方法,第六章介绍枚举、`match` 表达式和 `if let` 控制流结构。在 Rust 中,你将使用结构体和枚举创建自定义类型。 +第 5 章讨论结构体和方法,第 6 章介绍枚举、`match` 表达式和 `if let` 控制流结构。在 Rust 中,你将使用结构体和枚举创建自定义类型。 -第七章你会学习 Rust 的模块系统和私有性规则来组织代码和公有应用程序接口(Application Programming Interface, API)。第八章讨论了一些标准库提供的常见集合数据结构,比如 可变长数组(vector)、字符串和哈希 map。第九章探索了 Rust 的错误处理哲学和技术。 +第 7 章你会学习 Rust 的模块系统和私有性规则来组织代码和公有应用程序接口(Application Programming Interface, API)。第 8 章讨论了一些标准库提供的常见集合数据结构,比如 可变长数组(vector)、字符串和哈希 map。第 9 章探索了 Rust 的错误处理哲学和技术。 -第十章深入介绍泛型、trait 和生命周期,他们提供了定义出适用于多种类型的代码的能力。第十一章全部关于测试,即使 Rust 有安全保证,也需要测试确保程序逻辑正确。第十二章,我们构建了属于自己的在文件中搜索文本的命令行工具 `grep` 的子集功能实现。为此会利用之前章节讨论的很多概念。 +第 10 章深入介绍泛型、trait 和生命周期,他们提供了定义出适用于多种类型的代码的能力。第 11 章全部关于测试,即使 Rust 有安全保证,也需要测试确保程序逻辑正确。第 12 章,我们构建了属于自己的在文件中搜索文本的命令行工具 `grep` 的子集功能实现。为此会利用之前章节讨论的很多概念。 -第十三章探索了闭包和迭代器:Rust 中来自函数式编程语言的功能。第十四章会更深层次的理解 Cargo 并讨论向他人分享库的最佳实践。第十五章讨论标准库提供的智能指针以及启用这些功能的 trait。 +第 13 章探索了闭包和迭代器:Rust 中来自函数式编程语言的功能。第 14 章会更深层次的理解 Cargo 并讨论向他人分享库的最佳实践。第 15 章讨论标准库提供的智能指针以及启用这些功能的 trait。 -第十六章会学习不同的并发编程模型,并讨论 Rust 如何助你无畏的编写多线程程序。第十七章着眼于比较 Rust 风格与你可能熟悉的面向对象编程原则。 +第 16 章会学习不同的并发编程模型,并讨论 Rust 如何助你无畏的编写多线程程序。第 17 章着眼于比较 Rust 风格与你可能熟悉的面向对象编程原则。 -第十八章是关于模式和模式匹配的参考章节,它是在 Rust 程序中表达思想的有效方式。第十九章是一个高级主题大杂烩,包括 unsafe Rust、宏和更多关于生命周期、 trait、类型、函数和闭包的内容。 +第 18 章是关于模式和模式匹配的参考章节,它是在 Rust 程序中表达思想的有效方式。第 19 章是一个高级主题大杂烩,包括 unsafe Rust、宏和更多关于生命周期、 trait、类型、函数和闭包的内容。 -第二十章将会完成一个项目,我们会实现一个底层的、多线程的 web server! +第 20 章将会完成一个项目,我们会实现一个底层的、多线程的 web server! 最后是一些附录,包含了一些关于语言的参考风格格式的实用信息。附录 A 介绍了 Rust 的关键字。附录 B 介绍 Rust 的运算符和符号。附录 C 介绍标准库提供的派生 trait。附录 D 涉及了一些有用的开发工具,附录 E 介绍了 Rust 的不同版本。 diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 6b9fb77..2f1e783 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -6,7 +6,7 @@ ## 准备一个新项目 -要创建一个新项目,进入第一章中创建的 *projects* 目录,使用 Cargo 新建一个项目,如下: +要创建一个新项目,进入第 1 章中创建的 *projects* 目录,使用 Cargo 新建一个项目,如下: ```console $ cargo new guessing_game @@ -25,7 +25,7 @@ $ cd guessing_game 如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。 -正如第一章那样,`cargo new` 生成了一个 “Hello, world!” 程序。查看 *src/main.rs* 文件: +正如第 1 章那样,`cargo new` 生成了一个 “Hello, world!” 程序。查看 *src/main.rs* 文件: 文件名:src/main.rs @@ -65,7 +65,7 @@ $ cd guessing_game [prelude]: https://doc.rust-lang.org/std/prelude/index.html -如第一章所提及,`main` 函数是程序的入口点: +如第 1 章所提及,`main` 函数是程序的入口点: ```rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:main}} @@ -73,7 +73,7 @@ $ cd guessing_game `fn` 语法声明了一个新函数,`()` 表明没有参数,`{` 作为函数体的开始。 -第一章也提及了 `println!` 是一个在屏幕上打印字符串的宏: +第 1 章也提及了 `println!` 是一个在屏幕上打印字符串的宏: ```rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print}} @@ -95,7 +95,7 @@ $ cd guessing_game let foo = bar; ``` -这行代码新建了一个叫做 `foo` 的变量并把它绑定到值 `bar` 上。在 Rust 中,变量默认是不可变的。我们将会在第三章的 [“变量与可变性”][variables-and-mutability] 部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 `mut` 来使一个变量可变: +这行代码新建了一个叫做 `foo` 的变量并把它绑定到值 `bar` 上。在 Rust 中,变量默认是不可变的。我们将会在第 3 章的 [“变量与可变性”][variables-and-mutability] 部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 `mut` 来使一个变量可变: ```rust,ignore let foo = 5; // 不可变 @@ -171,7 +171,7 @@ io::stdin().read_line(&mut guess).expect("Failed to read line"); Rust 警告我们没有使用 `read_line` 的返回值 `Result`,说明有一个可能的错误没有处理。 -消除警告的正确做法是实际编写错误处理代码,不过由于我们就是希望程序在出现问题时立即崩溃,所以直接使用 `expect`。第九章会学习如何从错误中恢复。 +消除警告的正确做法是实际编写错误处理代码,不过由于我们就是希望程序在出现问题时立即崩溃,所以直接使用 `expect`。第 9 章会学习如何从错误中恢复。 ### 使用 `println!` 占位符打印值 diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md index e59346d..cccc65f 100644 --- a/src/ch03-05-control-flow.md +++ b/src/ch03-05-control-flow.md @@ -24,7 +24,7 @@ fn main() { -所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 的值是否小于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做 *arms*,就像第二章 [“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number] 部分中讨论到的 `match` 表达式中的分支一样。 +所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 的值是否小于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做 *arms*,就像第 2 章 [“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number] 部分中讨论到的 `match` 表达式中的分支一样。 也可以包含一个可选的 `else` 表达式来提供一个在条件为假时应当执行的代码块,这里我们就这么做了。如果不提供 `else` 表达式并且条件为假时,程序会直接忽略 `if` 代码块并继续执行下面的代码。 @@ -131,7 +131,7 @@ number is divisible by 3 当执行这个程序时,它按顺序检查每个 `if` 表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会输出 `number is divisible by 2`,更不会输出 `else` 块中的 `number is not divisible by 4, 3, or 2`。原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。 -使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if` 表达式,最好重构代码。为此,第六章会介绍一个强大的 Rust 分支结构(branching construct),叫做 `match`。 +使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if` 表达式,最好重构代码。为此,第 6 章会介绍一个强大的 Rust 分支结构(branching construct),叫做 `match`。 #### 在 `let` 语句中使用 `if` @@ -240,7 +240,7 @@ again! 符号 `^C` 代表你在这按下了ctrl-c。在 `^C` 之后你可能看到也可能看不到 `again!` ,这取决于在接收到终止信号时代码执行到了循环的何处。 -幸运的是,Rust 提供了另一种更可靠的退出循环的方式。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第二章猜数字游戏的 [“猜测正确后退出”][quitting-after-a-correct-guess] 部分使用过它来在用户猜对数字赢得游戏后退出程序。 +幸运的是,Rust 提供了另一种更可靠的退出循环的方式。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第 2 章猜数字游戏的 [“猜测正确后退出”][quitting-after-a-correct-guess] 部分使用过它来在用户猜对数字赢得游戏后退出程序。 #### 从循环返回 diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md index e7697c8..8be7abe 100644 --- a/src/ch04-01-what-is-ownership.md +++ b/src/ch04-01-what-is-ownership.md @@ -36,7 +36,7 @@ Rust 的核心功能(之一)是 **所有权**(*ownership*)。虽然该 ### 变量作用域 -我们已经在第二章完成一个 Rust 程序示例。既然我们已经掌握了基本语法,将不会在之后的例子中包含 `fn main() {` 代码,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个 `main` 函数中。这样,例子将显得更加简明,使我们可以关注实际细节而不是样板代码。 +我们已经在第 2 章完成一个 Rust 程序示例。既然我们已经掌握了基本语法,将不会在之后的例子中包含 `fn main() {` 代码,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个 `main` 函数中。这样,例子将显得更加简明,使我们可以关注实际细节而不是样板代码。 在所有权的第一个例子中,我们看看一些变量的 **作用域**(*scope*)。作用域是一个项(item)在程序中有效的范围。假设有这样一个变量: @@ -65,9 +65,9 @@ let s = "hello"; ### `String` 类型 -为了演示所有权的规则,我们需要一个比第三章 [“数据类型”][data-types] 中讲到的都要复杂的数据类型。前面介绍的类型都是存储在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。 +为了演示所有权的规则,我们需要一个比第 3 章 [“数据类型”][data-types] 中讲到的都要复杂的数据类型。前面介绍的类型都是存储在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。 -这里使用 `String` 作为例子,并专注于 `String` 与所有权相关的部分。这些方面也同样适用于标准库提供的或你自己创建的其他复杂数据类型。在第八章会更深入地讲解 `String`。 +这里使用 `String` 作为例子,并专注于 `String` 与所有权相关的部分。这些方面也同样适用于标准库提供的或你自己创建的其他复杂数据类型。在第 8 章会更深入地讲解 `String`。 我们已经见过字符串字面值,即被硬编码进程序里的字符串值。字符串字面值是很方便的,不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型被分配到堆上,所以能够存储在编译时未知大小的文本。可以使用 `from` 函数基于字符串字面值来创建 `String`,如下: @@ -75,7 +75,7 @@ let s = "hello"; let s = String::from("hello"); ``` -这两个冒号(`::`)是运算符,允许将特定的 `from` 函数置于 `String` 类型的命名空间(namespace)下,而不需要使用类似 `string_from` 这样的名字。在第五章的 [“方法语法”(“Method Syntax”)][method-syntax] 部分会着重讲解这个语法而且在第七章的 [“路径用于引用模块树中的项”][paths-module-tree] 中会讲到模块的命名空间。 +这两个冒号(`::`)是运算符,允许将特定的 `from` 函数置于 `String` 类型的命名空间(namespace)下,而不需要使用类似 `string_from` 这样的名字。在第 5 章的 [“方法语法”(“Method Syntax”)][method-syntax] 部分会着重讲解这个语法而且在第 7 章的 [“路径用于引用模块树中的项”][paths-module-tree] 中会讲到模块的命名空间。 **可以** 修改此类字符串 : @@ -200,7 +200,7 @@ error[E0382]: use of moved value: `s1` #### 变量与数据交互的方式(二):克隆 -如果我们 **确实** 需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。 +如果我们 **确实** 需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的通用函数。第 5 章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。 这是一个实际使用 `clone` 方法的例子: @@ -230,7 +230,7 @@ println!("x = {}, y = {}", x, y); 原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。 -Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型拥有 `Copy` trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。要学习如何为你的类型增加 `Copy` 注解,请阅读附录 C 中的 [“可派生的 trait”][derivable-traits]。 +Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第 10 章详细讲解 trait)。如果一个类型拥有 `Copy` trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。要学习如何为你的类型增加 `Copy` 注解,请阅读附录 C 中的 [“可派生的 trait”][derivable-traits]。 那么什么类型是 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的。如下是一些 `Copy` 的类型: diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md index 1b134a5..f365d3c 100644 --- a/src/ch04-02-references-and-borrowing.md +++ b/src/ch04-02-references-and-borrowing.md @@ -28,7 +28,7 @@ fn calculate_length(s: &String) -> usize { 图 4-5:`&String s` 指向 `String s1` 示意图 -> 注意:与使用 `&` 引用相反的操作是 **解引用**(*dereferencing*),它使用解引用运算符,`*`。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。 +> 注意:与使用 `&` 引用相反的操作是 **解引用**(*dereferencing*),它使用解引用运算符,`*`。我们将会在第 8 章遇到一些解引用运算符,并在第 15 章详细讨论解引用。 仔细看看这个函数调用: @@ -242,7 +242,7 @@ error[E0106]: missing lifetime specifier = help: consider giving it a 'static lifetime ``` -错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。第十章会详细介绍生命周期。不过,如果你不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键信息: +错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。第 10 章会详细介绍生命周期。不过,如果你不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键信息: ```text this function's return type contains a borrowed value, but there is no value for it to be borrowed from. diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md index 81abaea..ec8d6ea 100644 --- a/src/ch04-03-slices.md +++ b/src/ch04-03-slices.md @@ -42,7 +42,7 @@ let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { ``` -我们将在第十三章详细讨论迭代器。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装了 `iter` 的结果,将这些元素作为元组的一部分来返回。`enumerate` 返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 +我们将在第 13 章详细讨论迭代器。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装了 `iter` 的结果,将这些元素作为元组的一部分来返回。`enumerate` 返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,就像 Rust 中其他任何地方所做的一样。所以在 `for` 循环中,我们指定了一个模式,其中元组中的 `i` 是索引而元组中的 `&item` 是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`。 @@ -152,7 +152,7 @@ let slice = &s[0..len]; let slice = &s[..]; ``` -> 注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第八章的 [“使用字符串存储 UTF-8 编码的文本”][strings] 部分会更加全面的讨论 UTF-8 处理问题。 +> 注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第 8 章的 [“使用字符串存储 UTF-8 编码的文本”][strings] 部分会更加全面的讨论 UTF-8 处理问题。 在记住所有这些知识后,让我们重写 `first_word` 来返回一个 slice。“字符串 slice” 的类型声明写作 `&str`: @@ -291,12 +291,12 @@ let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; ``` -这个 slice 的类型是 `&[i32]`。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。 +这个 slice 的类型是 `&[i32]`。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。第 8 章讲到 vector 时会详细讨论这些集合。 ## 总结 所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。 -所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始第五章,来看看如何将多份数据组合进一个 `struct` 中。 +所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始第 5 章,来看看如何将多份数据组合进一个 `struct` 中。 [strings]: ch08-02-strings.html#storing-utf-8-encoded-text-with-strings diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md index f249e4a..90cf39b 100644 --- a/src/ch05-00-structs.md +++ b/src/ch05-00-structs.md @@ -1,4 +1,4 @@ # 使用结构体组织相关联的数据 -*struct*,或者 *structure*,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,*struct* 就像对象中的数据属性。在本章中,我们会对比元组与结构体的异同,演示结构体的用法,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。你可以在程序中基于结构体和枚举(*enum*)(在第六章介绍)创建新类型,以充分利用 Rust 的编译时类型检查。 +*struct*,或者 *structure*,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,*struct* 就像对象中的数据属性。在本章中,我们会对比元组与结构体的异同,演示结构体的用法,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。你可以在程序中基于结构体和枚举(*enum*)(在第 6 章介绍)创建新类型,以充分利用 Rust 的编译时类型检查。 diff --git a/src/ch05-01-defining-structs.md b/src/ch05-01-defining-structs.md index dd60d9a..348f212 100644 --- a/src/ch05-01-defining-structs.md +++ b/src/ch05-01-defining-structs.md @@ -1,6 +1,6 @@ ## 定义并实例化结构体 -结构体和我们在第三章讨论过的元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。 +结构体和我们在第 3 章讨论过的元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。 定义结构体,需要使用 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 **字段**(*field*)。例如,示例 5-1 展示了一个存储用户账号信息的结构体: @@ -170,7 +170,7 @@ let user2 = User { ### 使用没有命名字段的元组结构体来创建不同的类型 -也可以定义与元组(在第三章讨论过)类似的结构体,称为 **元组结构体**(*tuple structs*)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。 +也可以定义与元组(在第 3 章讨论过)类似的结构体,称为 **元组结构体**(*tuple structs*)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。 要定义元组结构体,以 `struct` 关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 `Color` 和 `Point` 元组结构体的定义和用法: @@ -186,13 +186,13 @@ let origin = Point(0, 0, 0); ### 没有任何字段的类单元结构体 -我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体**(*unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在第十章介绍 trait。 +我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体**(*unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在第 10 章介绍 trait。 > ### 结构体数据的所有权 > > 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。 > -> 可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期**(*lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样: +> 可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期**(*lifetimes*),这是一个第 10 章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样: > > 文件名: src/main.rs > @@ -230,7 +230,7 @@ let origin = Point(0, 0, 0); > | ^ expected lifetime parameter > ``` > -> 第十章会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 `String` 这类拥有所有权的类型来替代 `&str` 这样的引用以修正这个错误。 +> 第 10 章会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 `String` 这类拥有所有权的类型来替代 `&str` 这样的引用以修正这个错误。 部分,我们曾将 `Rng` trait 引入作用域并调用了 `rand::thread_rng` 函数: +接着,为了将 `rand` 定义引入项目包的作用域,我们加入一行 `use` 起始的包名,它以 `rand` 包名开头并列出了需要引入作用域的项。回忆一下第 2 章的 [“生成一个随机数”][rand] 部分,我们曾将 `Rng` trait 引入作用域并调用了 `rand::thread_rng` 函数: ```rust,ignore use rand::Rng; @@ -264,7 +264,7 @@ use std::collections::*; 这个 `use` 语句将 `std::collections` 中定义的所有公有项引入当前作用域。使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。 -glob 运算符经常用于测试模块 `tests` 中,这时会将所有内容引入作用域;我们将在第十一章 [“如何编写测试”][writing-tests] 部分讲解。glob 运算符有时也用于 prelude 模式;查看 [标准库中的文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes) 了解这个模式的更多细节。 +glob 运算符经常用于测试模块 `tests` 中,这时会将所有内容引入作用域;我们将在第 11 章 [“如何编写测试”][writing-tests] 部分讲解。glob 运算符有时也用于 prelude 模式;查看 [标准库中的文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes) 了解这个模式的更多细节。 [rand]: ch02-00-guessing-game-tutorial.html#生成一个随机数 -[writing-tests]: ch11-01-writing-tests.html#如何编写测试 \ No newline at end of file +[writing-tests]: ch11-01-writing-tests.html#如何编写测试 diff --git a/src/ch08-01-vectors.md b/src/ch08-01-vectors.md index 145b3cc..a5af411 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -12,7 +12,7 @@ let v: Vec = Vec::new(); 示例 8-1:新建一个空的 vector 来储存 `i32` 类型的值 -注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这一点非常重要。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,我们知道 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。 +注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这一点非常重要。vector 是用泛型实现的,第 10 章会涉及到如何对你自己的类型使用它们。现在,我们知道 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。 在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。示例 8-2 新建一个拥有值 `1`、`2` 和 `3` 的 `Vec`: @@ -39,7 +39,7 @@ v.push(8); 示例 8-3:使用 `push` 方法向 vector 增加值 -如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 `Vec` 注解。 +如第 3 章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 `Vec` 注解。 ### 丢弃 vector 时也会丢弃其所有元素 @@ -93,9 +93,9 @@ let does_not_exist = v.get(100); 当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 panic。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。 -当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! +当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第 6 章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! -一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的: +一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第 4 章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的: ```rust,ignore,does_not_compile let mut v = vec![1, 2, 3, 4, 5]; @@ -153,7 +153,7 @@ for i in &mut v { 示例8-9:遍历 vector 中元素的可变引用 -为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第十五章的 [“通过解引用运算符追踪指针的值”][deref] 部分会详细介绍解引用运算符。 +为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第 15 章的 [“通过解引用运算符追踪指针的值”][deref] 部分会详细介绍解引用运算符。 ### 使用枚举来储存多种类型 @@ -177,9 +177,9 @@ let row = vec![ 示例 8-10:定义一个枚举,以便能在 vector 中存放不同类型的数据 -Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。 +Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第 6 章讲到的那样。 -如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。 +如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第 17 章会讲到它。 现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 API 文档。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`! diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index a547909..c5575c8 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -1,12 +1,12 @@ ## 使用字符串存储 UTF-8 编码的文本 -第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多开发者所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的开发者就可能显得很困难了。 +第 4 章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多开发者所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的开发者就可能显得很困难了。 在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,由于人和计算机理解 `String` 数据方式的不同,`String` 索引是非常复杂的。 ### 什么是字符串? -在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。 +在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第 4 章讲到了 **字符串 slice**:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。 称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 `String` 和字符串 slice `&str` 类型,而不仅仅是其中之一。虽然本部分内容大多是关于 `String` ,不过这两个类型在 Rust 标准库中都被广泛使用,`String` 和字符串 slice 都是 UTF-8 编码。 @@ -124,11 +124,11 @@ let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用 fn add(self, s: &str) -> String { ``` -这并不是标准库中实际的签名;标准库中的 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。 +这并不是标准库中实际的签名;标准库中的 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第 10 章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。 首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下 —— 正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么示例 8-18 还能编译呢? -之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当 `add` 函数被调用时,Rust 使用了一个被称为 **解引用强制转换**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论解引用强制转换。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。 +之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当 `add` 函数被调用时,Rust 使用了一个被称为 **解引用强制转换**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第 15 章会更深入的讨论解引用强制转换。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。 其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。 diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index 5c62bec..9e5420c 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -60,7 +60,7 @@ map.insert(field_name, field_value); 当 `insert` 调用将 `field_name` 和 `field_value` 移动到哈希 map 中后,将不能使用这两个绑定。 -如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章 [“生命周期与引用有效性”][validating-references-with-lifetimes] 部分将会更多的讨论这个问题。 +如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第 10 章 [“生命周期与引用有效性”][validating-references-with-lifetimes] 部分将会更多的讨论这个问题。 ### 访问哈希 map 中的值 @@ -80,7 +80,7 @@ let score = scores.get(&team_name); 示例 8-23:访问哈希 map 中储存的蓝队分数 -这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第六章提到的方法之一来处理 `Option`。 +这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第 6 章提到的方法之一来处理 `Option`。 可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 `for` 循环: @@ -174,7 +174,7 @@ println!("{:?}", map); ### 哈希函数 -`HashMap` 默认使用一种 “密码学安全的”(“cryptographically strong” )[^siphash] 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 +`HashMap` 默认使用一种 “密码学安全的”(“cryptographically strong” )[^siphash] 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第 10 章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 [^siphash]: [https://www.131002.net/siphash/siphash.pdf](https://www.131002.net/siphash/siphash.pdf) diff --git a/src/ch09-02-recoverable-errors-with-result.md b/src/ch09-02-recoverable-errors-with-result.md index b7d691c..7620c42 100644 --- a/src/ch09-02-recoverable-errors-with-result.md +++ b/src/ch09-02-recoverable-errors-with-result.md @@ -2,7 +2,7 @@ 大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。 -回忆一下第二章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`: +回忆一下第 2 章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`: [handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误 @@ -13,7 +13,7 @@ enum Result { } ``` -`T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 +`T` 和 `E` 是泛型类型参数;第 10 章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-3 所示打开一个文件: @@ -55,7 +55,7 @@ error[E0308]: mismatched types 当 `File::open` 成功的情况下,变量 `f` 的值将会是一个包含文件句柄的 `Ok` 实例。在失败的情况下,`f` 的值会是一个包含更多关于出现了何种错误信息的 `Err` 实例。 -我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具(第六章学习过的 `match` 表达式)处理 `Result` 的例子: +我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具(第 6 章学习过的 `match` 表达式)处理 `Result` 的例子: 文件名: src/main.rs @@ -121,7 +121,7 @@ fn main() { 我们希望在内层 `match` 中检查的条件是 `error.kind()` 的返回值是否为 `ErrorKind`的 `NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内层 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外层 `match` 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。 -这里有好多 `match`!`match` 确实很强大,不过也非常的基础。第十三章我们会介绍闭包(closure)。`Result` 有很多接受闭包的方法,并采用 `match` 表达式实现。一个更老练的 Rustacean 可能会这么写: +这里有好多 `match`!`match` 确实很强大,不过也非常的基础。第 13 章我们会介绍闭包(closure)。`Result` 有很多接受闭包的方法,并采用 `match` 表达式实现。一个更老练的 Rustacean 可能会这么写: ```rust,ignore use std::fs::File; @@ -140,7 +140,7 @@ fn main() { } ``` -虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。 +虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第 13 章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。 ### 失败时 panic 的简写:`unwrap` 和 `expect` @@ -337,7 +337,7 @@ fn main() -> Result<(), Box> { } ``` -`Box` 被称为 “trait 对象”(trait object),第十七章 [“为使用不同类型的值而设计的 trait 对象”][trait-objects] 部分会做介绍。目前可以理解 `Box` 为使用 `?` 时 `main` 允许返回的 “任何类型的错误”。 +`Box` 被称为 “trait 对象”(trait object),第 17 章 [“为使用不同类型的值而设计的 trait 对象”][trait-objects] 部分会做介绍。目前可以理解 `Box` 为使用 `?` 时 `main` 允许返回的 “任何类型的错误”。 现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到他们各自适合哪些场景的话题了。 diff --git a/src/ch09-03-to-panic-or-not-to-panic.md b/src/ch09-03-to-panic-or-not-to-panic.md index 4cb38c9..4e2287e 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -42,7 +42,7 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); ### 创建自定义类型进行有效性验证 -让我们使用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第二章的猜猜看游戏,我们的代码要求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的,我们只验证它是否为正。在这种情况下,其影响并不是很严重:“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。 +让我们使用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第 2 章的猜猜看游戏,我们的代码要求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的,我们只验证它是否为正。在这种情况下,其影响并不是很严重:“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。 一种实现方式是将猜测解析成 `i32` 而不仅仅是 `u32`,来默许输入负数,接着检查数字是否在范围内: @@ -98,7 +98,7 @@ impl Guess { 首先,我们定义了一个包含 `i32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。 -接着在 `Guess` 上实现了一个叫做 `new` 的关联函数来创建 `Guess` 的实例。`new` 定义为接收一个 `i32` 类型的参数 `value` 并返回一个 `Guess`。`new` 函数中代码的测试确保了其值是在 1 到 100 之间的。如果 `value` 没有通过测试则调用 `panic!`,这会警告调用这个函数的开发者有一个需要修改的 bug,因为创建一个 `value` 超出范围的 `Guess` 将会违反 `Guess::new` 所遵循的契约。`Guess::new` 会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明 `panic!` 可能性的相关规则。如果 `value` 通过了测试,我们新建一个 `Guess`,其字段 `value` 将被设置为参数 `value` 的值,接着返回这个 `Guess`。 +接着在 `Guess` 上实现了一个叫做 `new` 的关联函数来创建 `Guess` 的实例。`new` 定义为接收一个 `i32` 类型的参数 `value` 并返回一个 `Guess`。`new` 函数中代码的测试确保了其值是在 1 到 100 之间的。如果 `value` 没有通过测试则调用 `panic!`,这会警告调用这个函数的开发者有一个需要修改的 bug,因为创建一个 `value` 超出范围的 `Guess` 将会违反 `Guess::new` 所遵循的契约。`Guess::new` 会出现 panic 的条件应该在其公有 API 文档中被提及;第 14 章会涉及到在 API 文档中表明 `panic!` 可能性的相关规则。如果 `value` 通过了测试,我们新建一个 `Guess`,其字段 `value` 将被设置为参数 `value` 的值,接着返回这个 `Guess`。 接着,我们实现了一个借用了 `self` 的方法 `value`,它没有任何其他参数并返回一个 `i32`。这类方法有时被称为 *getter*,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 `Guess` 结构体的 `value` 字段是私有的。私有的字段 `value` 是很重要的,这样使用 `Guess` 结构体的代码将不允许直接设置 `value` 的值:调用者 **必须** 使用 `Guess::new` 方法来创建一个 `Guess` 的实例,这就确保了不会存在一个 `value` 没有通过 `Guess::new` 函数的条件检查的 `Guess`。 diff --git a/src/ch10-00-generics.md b/src/ch10-00-generics.md index 4d9bd7f..153baa8 100644 --- a/src/ch10-00-generics.md +++ b/src/ch10-00-generics.md @@ -2,7 +2,7 @@ 每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。 -同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第六章的 `Option`,第八章的 `Vec` 和 `HashMap`,以及第九章的 `Result` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法! +同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第 6 章的 `Option`,第 8 章的 `Vec` 和 `HashMap`,以及第 9 章的 `Result` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法! 首先,我们将回顾一下提取函数以减少代码重复的机制。接下来,我们将使用相同的技术,从两个仅参数类型不同的函数中创建一个泛型函数。我们也会讲到结构体和枚举定义中的泛型。 diff --git a/src/ch10-01-syntax.md b/src/ch10-01-syntax.md index 729199c..2bdd1fb 100644 --- a/src/ch10-01-syntax.md +++ b/src/ch10-01-syntax.md @@ -188,7 +188,7 @@ fn main() { ### 枚举定义中的泛型 -和结构体类似,枚举也可以在成员中存放泛型数据类型。第六章我们曾用过标准库提供的 `Option` 枚举,这里再回顾一下: +和结构体类似,枚举也可以在成员中存放泛型数据类型。第 6 章我们曾用过标准库提供的 `Option` 枚举,这里再回顾一下: ```rust enum Option { @@ -199,7 +199,7 @@ enum Option { 现在这个定义应该更容易理解了。如你所见 `Option` 是一个拥有泛型 `T` 的枚举,它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的 `None`。通过 `Option` 枚举可以表达有一个可能的值的抽象概念,同时因为 `Option` 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。 -枚举也可以拥有多个泛型类型。第九章使用过的 `Result` 枚举定义就是一个这样的例子: +枚举也可以拥有多个泛型类型。第 9 章使用过的 `Result` 枚举定义就是一个这样的例子: ```rust enum Result { @@ -214,7 +214,7 @@ enum Result { ### 方法定义中的泛型 -在为结构体和枚举实现方法时(像第五章那样),一样也可以用泛型。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point`,和在其上实现的名为 `x` 的方法。 +在为结构体和枚举实现方法时(像第 5 章那样),一样也可以用泛型。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point`,和在其上实现的名为 `x` 的方法。 文件名: src/main.rs diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index 7b4b407..e13a2ff 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -260,7 +260,7 @@ fn returns_summarizable() -> impl Summary { 通过使用 `impl Summary` 作为返回值类型,我们指定了 `returns_summarizable` 函数返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型。在这个例子中 `returns_summarizable` 返回了一个 `Tweet`,不过调用方并不知情。 -返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第十三章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的指定函数返回一个 `Iterator` 而无需写出实际的冗长的类型。 +返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第 13 章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的指定函数返回一个 `Iterator` 而无需写出实际的冗长的类型。 不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 `impl Summary`,但是返回了 `NewsArticle` 或 `Tweet` 就行不通: @@ -285,7 +285,7 @@ fn returns_summarizable(switch: bool) -> impl Summary { } ``` -这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。第十七章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。 +这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。第 17 章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。 ### 使用 trait bounds 来修复 `largest` 函数 @@ -329,7 +329,7 @@ error[E0507]: cannot move out of borrowed content | cannot move out of borrowed content ``` -错误的核心是 `cannot move out of type [T], a non-copy slice`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章 [“只在栈上的数据:拷贝”][stack-only-data-copy] 部分讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的。这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中,这导致了上面的错误。 +错误的核心是 `cannot move out of type [T], a non-copy slice`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第 4 章 [“只在栈上的数据:拷贝”][stack-only-data-copy] 部分讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的。这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中,这导致了上面的错误。 为了只对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-15 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` **和** `Copy` 这两个 trait,例如 `i32` 和 `char`: diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index 07b317d..a55470f 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -1,6 +1,6 @@ ## 生命周期与引用有效性 -当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 +当在第 4 章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。 @@ -102,7 +102,7 @@ fn main() { 请注意,这个函数获取作为引用的字符串 slice,因为我们不希望 `longest` 函数获取参数的所有权。我们期望该函数接受 `String` 的 slice(参数 `string1` 的类型)和字符串字面值(包含于参数 `string2`) -参考第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。 +参考第 4 章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。 如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译: @@ -324,7 +324,7 @@ fn main() { ### 生命周期省略(Lifetime Elision) -现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期注解却能编译成功: +现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第 4 章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期注解却能编译成功: 文件名: src/lib.rs @@ -482,7 +482,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st 这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型,你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率! -你可能不会相信,这个话题还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! +你可能不会相信,这个话题还有更多需要学习的内容:第 17 章会讨论 trait 对象,这是另一种使用 trait 的方式。第 19 章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! [references-and-borrowing]: ch04-02-references-and-borrowing.html#references-and-borrowing diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index 19cbd81..758be85 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -10,9 +10,9 @@ Rust 中的测试函数是用来验证非测试代码是否按照期望的方式 ### 测试函数剖析 -作为最简单例子,Rust 中的测试就是一个带有 `test` 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第五章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 `test` 属性的函数,并报告每一个测试是通过还是失败。 +作为最简单例子,Rust 中的测试就是一个带有 `test` 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第 5 章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 `test` 属性的函数,并报告每一个测试是通过还是失败。 -第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块! +第 7 章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块! 我们会通过实验那些自动生成的测试模版而不是实际编写测试代码来探索测试如何工作的一些方面。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。 @@ -75,7 +75,7 @@ Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这 [bench]: https://doc.rust-lang.org/unstable-book/library-features/test.html -测试输出中的以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 [“文档注释作为测试”][doc-comments] 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。 +测试输出中的以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第 14 章的 [“文档注释作为测试”][doc-comments] 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。 让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样: @@ -101,7 +101,7 @@ test tests::exploration ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` -让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏。写入新测试 `another` 后, `src/lib.rs` 现在看起来如示例 11-3 所示: +让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第 9 章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏。写入新测试 `another` 后, `src/lib.rs` 现在看起来如示例 11-3 所示: 文件名: src/lib.rs @@ -156,7 +156,7 @@ error: test failed `assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个求值为布尔值的参数。如果值是 `true`,`assert!` 什么也不做,同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。 -回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。 +回忆一下第 5 章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。 文件名: src/lib.rs @@ -175,7 +175,7 @@ impl Rectangle { } ``` -示例 11-5:第五章中 `Rectangle` 结构体和其 `can_hold` 方法 +示例 11-5:第 5 章中 `Rectangle` 结构体和其 `can_hold` 方法 `can_hold` 方法返回一个布尔值,这意味着它完美符合 `assert!` 宏的使用场景。在示例 11-6 中,让我们编写一个 `can_hold` 方法的测试来作为练习,这里创建一个长为 8 宽为 7 的 `Rectangle` 实例,并假设它可以放得下另一个长为 5 宽为 1 的 `Rectangle` 实例: @@ -199,7 +199,7 @@ mod tests { 示例 11-6:一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形 -注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 [“路径用于引用模块树中的项”][paths-for-referring-to-an-item-in-the-module-tree] 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。 +注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第 7 章 [“路径用于引用模块树中的项”][paths-for-referring-to-an-item-in-the-module-tree] 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。 我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待! @@ -358,11 +358,11 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out `assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。 -`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C [“可派生 trait”][derivable-traits] 中有更多关于这些和其他派生 trait 的详细信息。 +`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第 5 章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C [“可派生 trait”][derivable-traits] 中有更多关于这些和其他派生 trait 的详细信息。 ### 自定义失败信息 -你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的 [“使用 `+` 运算符或 `format!` 宏拼接字符串”][concatenation-with-the--operator-or-the-format-macro] 部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。 +你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第 8 章的 [“使用 `+` 运算符或 `format!` 宏拼接字符串”][concatenation-with-the--operator-or-the-format-macro] 部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。 例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中: @@ -440,7 +440,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. ### 使用 `should_panic` 检查 panic -除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-10 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。 +除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第 9 章示例 9-10 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。 可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。 diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md index 368f805..91cbdf7 100644 --- a/src/ch11-03-test-organization.md +++ b/src/ch11-03-test-organization.md @@ -137,9 +137,9 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件以便更好的组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。 -将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。然而,正如你在第七章中学习的如何将代码分为模块和文件的知识,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。 +将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。然而,正如你在第 7 章中学习的如何将代码分为模块和文件的知识,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。 -当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中不同文件的行为就会显得很明显。例如,如果我们可以创建 一个*tests/common.rs* 文件并创建一个名叫 `setup` 的函数,我们希望这个函数能被多个测试文件的测试函数调用: +当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第 7 章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中不同文件的行为就会显得很明显。例如,如果我们可以创建 一个*tests/common.rs* 文件并创建一个名叫 `setup` 的函数,我们希望这个函数能被多个测试文件的测试函数调用: 文件名: tests/common.rs diff --git a/src/ch12-00-an-io-project.md b/src/ch12-00-an-io-project.md index 11338b5..3fbb47b 100644 --- a/src/ch12-00-an-io-project.md +++ b/src/ch12-00-an-io-project.md @@ -10,13 +10,13 @@ Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使 我们的 `grep` 项目将会结合之前所学的一些内容: -- 代码组织(使用 [第七章][ch7] 学习的模块) -- vector 和字符串([第八章][ch8],集合) -- 错误处理([第九章][ch9]) -- 合理的使用 trait 和生命周期([第十章][ch10]) -- 测试([第十一章][ch11]) +- 代码组织(使用 [第 7 章][ch7] 学习的模块) +- vector 和字符串([第 8 章][ch8],集合) +- 错误处理([第 9 章][ch9]) +- 合理的使用 trait 和生命周期([第 10 章][ch10]) +- 测试([第 11 章][ch11]) -另外还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在 [第十三章][ch13] 和 [第十七章][ch17] 中详细介绍。 +另外还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在 [第 13 章][ch13] 和 [第 17 章][ch17] 中详细介绍。 [ch7]: ch07-00-managing-growing-projects-with-packages-crates-and-modules.html [ch8]: ch08-00-common-collections.html diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index 597bdf2..2fc1c7f 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -18,7 +18,7 @@ $ cargo run searchstring example-filename.txt ### 读取参数值 -为了确保 `minigrep` 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数,也就是 `std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器**(*iterator*)。我们会在 [第十三章][ch13] 全面的介绍它们。但是现在只需理解迭代器的两个细节:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个集合,比如包含所有迭代器产生元素的 vector。 +为了确保 `minigrep` 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数,也就是 `std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器**(*iterator*)。我们会在 [第 13 章][ch13] 全面的介绍它们。但是现在只需理解迭代器的两个细节:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个集合,比如包含所有迭代器产生元素的 vector。 使用示例 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。 @@ -35,7 +35,7 @@ fn main() { 示例 12-1:将命令行参数收集到一个 vector 中并打印出来 -首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如 [第七章][ch7-idiomatic-use] 讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些,因为 `args` 容易被错认成一个定义于当前模块的函数。 +首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如 [第 7 章][ch7-idiomatic-use] 讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些,因为 `args` 容易被错认成一个定义于当前模块的函数。 > ### `args` 函数和无效的 Unicode > diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 849a96c..acb2e73 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -110,7 +110,7 @@ fn parse_config(args: &[String]) -> Config { > #### 使用 `clone` 的权衡取舍 > -> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 `clone` 来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用 `clone` 是完全可以接受的。 +> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 `clone` 来解决所有权问题。在关于迭代器的第 13 章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用 `clone` 是完全可以接受的。 我们更新 `main` 将 `parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `query` 和 `filename` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。 @@ -202,7 +202,7 @@ thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如 [第九章][ch9-error-guidelines] 所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 [`Result`][ch9-result]。 +这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如 [第 9 章][ch9-error-guidelines] 所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第 9 章学习的另一个技术 —— 返回一个可以表明成功或错误的 [`Result`][ch9-result]。 #### 从 `new` 中返回 `Result` 而不是调用 `panic!` @@ -229,7 +229,7 @@ impl Config { 示例 12-9:从 `Config::new` 中返回 `Result` -现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。 +现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第 10 章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。 `new` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。 @@ -257,7 +257,7 @@ fn main() { 示例 12-10:如果新建 `Config` 失败则使用错误码退出 -在上面的示例中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第十三章][ch13] 会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。 +在上面的示例中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第 13 章][ch13] 会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。 我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试: @@ -327,9 +327,9 @@ fn run(config: Config) -> Result<(), Box> { 这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 -对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。[第十七章][ch17] 会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态的”(“dynamic”)的缩写。 +对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。[第 17 章][ch17] 会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态的”(“dynamic”)的缩写。 -第二个改变是去掉了 `expect` 调用并替换为 [第九章][ch9-question-mark] 讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。 +第二个改变是去掉了 `expect` 调用并替换为 [第 9 章][ch9-question-mark] 讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。 第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是惯用的做法,表明调用 `run` 函数只是为了它的副作用;函数并没有返回什么有意义的值。 diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 9d4a84f..455aedc 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -15,7 +15,7 @@ ### 编写失败测试 -去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要他们了。接着我们会像 [第十一章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译: +去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要他们了。接着我们会像 [第 11 章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译: 文件名: src/lib.rs @@ -60,7 +60,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 12-16:刚好足够使测试通过编译的 `search` 函数定义 -注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下 [第十章][ch10-lifetimes] 中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query`) slice 的字符串 slice。 +注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下 [第 10 章][ch10-lifetimes] 中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query`) slice 的字符串 slice。 换句话说,我们告诉 Rust 函数 `search` 返回的数据将与 `search` 函数中的参数 `contents` 的数据存在的一样久。这是非常重要的!为了使这个引用有效那么 **被** slice 引用的数据也需要保持有效;如果编译器认为我们是在创建 `query` 而不是 `contents` 的字符串 slice,那么安全检查将是不正确的。 @@ -80,7 +80,7 @@ parameter Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。 -其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中 [“生命周期与引用有效性”][validating-references-with-lifetimes] 部分做对比。 +其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第 10 章中 [“生命周期与引用有效性”][validating-references-with-lifetimes] 部分做对比。 现在运行测试: @@ -142,7 +142,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 12-17:遍历 `contents` 的每一行 -`lines` 方法返回一个迭代器。[第十三章][ch13-iterators] 会深入了解迭代器,不过我们已经在 [示例 3-5][ch3-iter] 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 +`lines` 方法返回一个迭代器。[第 13 章][ch13-iterators] 会深入了解迭代器,不过我们已经在 [示例 3-5][ch3-iter] 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 #### 用查询字符串搜索每一行 @@ -197,7 +197,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 测试通过了,它可以工作了! -现在正是可以考虑重构的时机,在保证测试通过,保持功能不变的前提下重构 `search` 函数。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 +现在正是可以考虑重构的时机,在保证测试通过,保持功能不变的前提下重构 `search` 函数。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第 13 章将回到这个例子并深入探索迭代器并看看如何改进代码。 #### 在 `run` 函数中使用 `search` 函数 diff --git a/src/ch13-00-functional-features.md b/src/ch13-00-functional-features.md index 4865090..5a91c78 100644 --- a/src/ch13-00-functional-features.md +++ b/src/ch13-00-functional-features.md @@ -8,7 +8,7 @@ Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著 * **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构 * **迭代器**(*Iterators*),一种处理元素序列的方式 -* 如何使用这些功能来改进第十二章的 I/O 项目。 +* 如何使用这些功能来改进第 12 章的 I/O 项目。 * 这两个功能的性能。(**剧透警告:** 他们的速度超乎你的想象!) 还有其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。 diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index b6b3387..7cc48eb 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -51,7 +51,7 @@ fn main() { 示例 13-2:`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入 -出于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: +出于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第 2 章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: 现在有了执行上下文,让我们编写算法。示例 13-3 中的 `generate_workout` 函数包含本例中我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中进行: @@ -290,7 +290,7 @@ error[E0308]: mismatched types 幸运的是,还有另一个可用的方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation* *(惰性求值)*。 -为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 +为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第 10 章讨论的那样使用泛型和 trait bound。 `Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn`、`FnMut` 或 `FnOnce` 中的一个。在 [“闭包会捕获其环境”](#闭包会捕获其环境) 部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。 @@ -533,7 +533,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ... 如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。 -第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动;注意这些代码还不能编译: +第 16 章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动;注意这些代码还不能编译: 文件名: src/main.rs diff --git a/src/ch13-02-iterators.md b/src/ch13-02-iterators.md index 7d6632d..0711b44 100644 --- a/src/ch13-02-iterators.md +++ b/src/ch13-02-iterators.md @@ -12,7 +12,7 @@ let v1_iter = v1.iter(); 示例 13-13:创建一个迭代器 -一旦创建迭代器之后,可以选择用多种方式利用它。在第三章的示例 3-5 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直没有具体讨论调用 `iter` 到底具体做了什么。 +一旦创建迭代器之后,可以选择用多种方式利用它。在第 3 章的示例 3-5 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直没有具体讨论调用 `iter` 到底具体做了什么。 示例 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值: @@ -46,7 +46,7 @@ pub trait Iterator { } ``` -注意这里有一个我们还未讲到的新语法:`type Item` 和 `Self::Item`,他们定义了 trait 的 **关联类型**(*associated type*)。第十九章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。 +注意这里有一个我们还未讲到的新语法:`type Item` 和 `Self::Item`,他们定义了 trait 的 **关联类型**(*associated type*)。第 19 章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。 `next` 是 `Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。 @@ -130,7 +130,7 @@ and do nothing unless consumed 示例 13-17 中的代码实际上并没有做任何事;所指定的闭包从未被调用过。警告提醒了我们为什么:迭代器适配器是惰性的,而这里我们需要消费迭代器。 -为了修复这个警告并消费迭代器获取有用的结果,我们将使用第十二章示例 12-1 结合 `env::args` 使用的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。 +为了修复这个警告并消费迭代器获取有用的结果,我们将使用第 12 章示例 12-1 结合 `env::args` 使用的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。 在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加 1 的结果: @@ -251,7 +251,7 @@ impl Iterator for Counter { 示例 13-21:在 `Counter` 结构体上实现 `Iterator` trait -这里将迭代器的关联类型 `Item` 设置为 `u32`,意味着迭代器会返回 `u32` 值集合。再一次,这里仍无需担心关联类型,第十九章会讲到。 +这里将迭代器的关联类型 `Item` 设置为 `u32`,意味着迭代器会返回 `u32` 值集合。再一次,这里仍无需担心关联类型,第 19 章会讲到。 我们希望迭代器对其内部状态加一,这也就是为何将 `count` 初始化为 0:我们希望迭代器首先返回 1。如果 `count` 值小于 6,`next` 会返回封装在 `Some` 中的当前值,不过如果 `count` 大于或等于 6,迭代器会返回 `None`。 diff --git a/src/ch13-03-improving-our-io-project.md b/src/ch13-03-improving-our-io-project.md index 7af251d..d14163c 100644 --- a/src/ch13-03-improving-our-io-project.md +++ b/src/ch13-03-improving-our-io-project.md @@ -1,10 +1,10 @@ ## 改进 I/O 项目 -有了这些关于迭代器的新知识,我们可以使用迭代器来改进第十二章中 I/O 项目的实现来使得代码更简洁明了。让我们看看迭代器如何能够改进 `Config::new` 函数和 `search` 函数的实现。 +有了这些关于迭代器的新知识,我们可以使用迭代器来改进第 12 章中 I/O 项目的实现来使得代码更简洁明了。让我们看看迭代器如何能够改进 `Config::new` 函数和 `search` 函数的实现。 ### 使用迭代器并去掉 `clone` -在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中重现了第十二章结尾示例 12-23 中 `Config::new` 函数的实现: +在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中重现了第 12 章结尾示例 12-23 中 `Config::new` 函数的实现: 文件名: src/lib.rs @@ -25,7 +25,7 @@ impl Config { } ``` -示例 13-24:重现第十二章结尾的 `Config::new` 函数 +示例 13-24:重现第 12 章结尾的 `Config::new` 函数 那时我们说过不必担心低效的 `clone` 调用了,因为将来可以对他们进行改进。好吧,就是现在! @@ -54,7 +54,7 @@ fn main() { } ``` -修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。在更新 `Config::new` 之前这些代码还不能编译: +修改第 12 章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。在更新 `Config::new` 之前这些代码还不能编译: 文件名: src/main.rs @@ -130,7 +130,7 @@ impl Config { ### 使用迭代器适配器来使代码更简明 -I/O 项目中其他可以利用迭代器的地方是 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义: +I/O 项目中其他可以利用迭代器的地方是 `search` 函数,示例 13-28 中重现了第 12 章结尾示例 12-19 中此函数的定义: 文件名: src/lib.rs diff --git a/src/ch14-02-publishing-to-crates-io.md b/src/ch14-02-publishing-to-crates-io.md index 1f3cc59..03314a9 100644 --- a/src/ch14-02-publishing-to-crates-io.md +++ b/src/ch14-02-publishing-to-crates-io.md @@ -6,7 +6,7 @@ Rust 和 Cargo 有一些帮助别人更方便找到和使用你发布的包的 ### 编写有用的文档注释 -准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(_documentation comments_),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的开发者理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。 +准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第 3 章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(_documentation comments_),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的开发者理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。 文档注释使用三斜杠 `///` 而不是两斜杆以支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释: @@ -44,7 +44,7 @@ pub fn add_one(x: i32) -> i32 { - **Panics**:这个函数可能会 `panic!` 的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。 - **Errors**:如果这个函数返回 `Result`,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。 -- **Safety**:如果这个函数使用 `unsafe` 代码(这会在第十九章讨论),这一部分应该会涉及到期望函数调用者支持的确保 `unsafe` 块中代码正常工作的不变条件(invariants)。 +- **Safety**:如果这个函数使用 `unsafe` 代码(这会在第 19 章讨论),这一部分应该会涉及到期望函数调用者支持的确保 `unsafe` 块中代码正常工作的不变条件(invariants)。 大部分文档注释不需要所有这些部分,不过这是一个提醒你检查调用你代码的人有兴趣了解的内容的列表。 @@ -95,7 +95,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ### 使用 `pub use` 导出合适的公有 API -第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而你开发时候使用的文件架构可能并不方便用户。你的结构可能是一个包含多个层级的分层结构,不过这对于用户来说并不方便。这是因为想要使用被定义在很深层级中的类型的人可能很难发现这些类型的存在。他们也可能会厌烦使用 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 +第 7 章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而你开发时候使用的文件架构可能并不方便用户。你的结构可能是一个包含多个层级的分层结构,不过这对于用户来说并不方便。这是因为想要使用被定义在很深层级中的类型的人可能很难发现这些类型的存在。他们也可能会厌烦使用 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如果模块层级过大他们可能会难以找到所需的部分。 diff --git a/src/ch14-03-cargo-workspaces.md b/src/ch14-03-cargo-workspaces.md index a2fb5bb..eb2ca36 100644 --- a/src/ch14-03-cargo-workspaces.md +++ b/src/ch14-03-cargo-workspaces.md @@ -1,6 +1,6 @@ ## Cargo 工作空间 -第十二章中,我们构建一个包含二进制 crate 和库 crate 的包。你可能会发现,随着项目开发的深入,库 crate 持续增大,而你希望将其进一步拆分成多个库 crate。对于这种情况,Cargo 提供了一个叫 **工作空间**(*workspaces*)的功能,它可以帮助我们管理多个相关的协同开发的包。 +第 12 章中,我们构建一个包含二进制 crate 和库 crate 的包。你可能会发现,随着项目开发的深入,库 crate 持续增大,而你希望将其进一步拆分成多个库 crate。对于这种情况,Cargo 提供了一个叫 **工作空间**(*workspaces*)的功能,它可以帮助我们管理多个相关的协同开发的包。 ### 创建工作空间 diff --git a/src/ch14-04-installing-binaries.md b/src/ch14-04-installing-binaries.md index 570387a..a127d5b 100644 --- a/src/ch14-04-installing-binaries.md +++ b/src/ch14-04-installing-binaries.md @@ -4,7 +4,7 @@ 所有来自 `cargo install` 的二进制文件都安装到 Rust 安装根目录的 *bin* 文件夹中。如果你使用 *rustup.rs* 安装的 Rust 且没有自定义任何配置,这将是 `$HOME/.cargo/bin`。确保将这个目录添加到 `$PATH` 环境变量中就能够运行通过 `cargo install` 安装的程序了。 -例如,第十二章提到的叫做 `ripgrep` 的用于搜索文件的 `grep` 的 Rust 实现。如果想要安装 `ripgrep`,可以运行如下: +例如,第 12 章提到的叫做 `ripgrep` 的用于搜索文件的 `grep` 的 Rust 实现。如果想要安装 `ripgrep`,可以运行如下: ```text $ cargo install ripgrep diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index 3918f83..f05f6d7 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -1,12 +1,12 @@ # 智能指针 -**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。 +**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第 4 章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。 另一方面,**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中不同的智能指针提供了多于引用的额外功能。本章将会探索的一个例子便是 **引用计数** (*reference counting*)智能指针类型,其允许数据有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。 在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 **拥有** 他们指向的数据。 -实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。 +实际上本书中已经出现过一些智能指针,比如第 8 章的 `String` 和 `Vec`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。 智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。 diff --git a/src/ch15-01-box.md b/src/ch15-01-box.md index e08f021..1bb0025 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -1,6 +1,6 @@ ## 使用`Box `指向堆上的数据 -最简单直接的智能指针是 _box_,其类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 +最简单直接的智能指针是 _box_,其类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第 4 章。 除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景: @@ -8,7 +8,7 @@ - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 - 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 -我们会在 “box 允许创建递归类型” 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(_trait object_),第十七章刚好有一整个部分 “为使用不同类型的值而设计的 trait 对象” 专门讲解这个主题。所以这里所学的内容会在第十七章再次用上! +我们会在 “box 允许创建递归类型” 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(_trait object_),第 17 章刚好有一整个部分 “为使用不同类型的值而设计的 trait 对象” 专门讲解这个主题。所以这里所学的内容会在第 17 章再次用上! ### 使用 `Box` 在堆上储存数据 @@ -43,7 +43,7 @@ _cons list_ 是一个来源于 Lisp 编程语言及其方言的数据结构。 cons 函数的概念涉及到更常见的函数式编程术语;“将 _x_ 与 _y_ 连接” 通常意味着构建一个新的容器而将 _x_ 的元素放在新容器的开头,其后则是容器 _y_ 的元素。 -cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 +cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第 6 章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 @@ -60,7 +60,7 @@ enum List { 示例 15-2:第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举 -> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 +> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型,正如第 10 章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 使用这个 cons list 来储存列表 `1, 2, 3` 将看起来如示例 15-3 所示: @@ -99,7 +99,7 @@ error[E0072]: recursive type `List` has infinite size ### 计算非递归类型的大小 -回忆一下第六章讨论枚举定义时示例 6-2 中定义的 `Message` 枚举: +回忆一下第 6 章讨论枚举定义时示例 6-2 中定义的 `Message` 枚举: ```rust enum Message { @@ -159,6 +159,6 @@ fn main() { 图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 -box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。 +box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第 17 章看到 box 的更多应用场景。 `Box` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box` 值被当作引用对待。当 `Box` 值离开作用域时,由于 `Box` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。让我们更详细的探索一下这两个 trait。这两个 trait 对于在本章余下讨论的其他智能指针所提供的功能中,将会更为重要。 diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 07f01dc..624a61f 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -113,7 +113,7 @@ error[E0614]: type `MyBox<{integer}>` cannot be dereferenced ### 通过实现 `Deref` trait 将某类型像引用一样处理 -如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: +如第 10 章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: 文件名: src/main.rs @@ -133,7 +133,7 @@ impl Deref for MyBox { 示例 15-10:`MyBox` 上的 `Deref` 实现 -`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。 +`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第 19 章会详细介绍。 `deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-9 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! diff --git a/src/ch15-04-rc.md b/src/ch15-04-rc.md index 5ccc39c..cb52c6b 100644 --- a/src/ch15-04-rc.md +++ b/src/ch15-04-rc.md @@ -8,7 +8,7 @@ `Rc` 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。 -注意 `Rc` 只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。 +注意 `Rc` 只能用于单线程场景;第 16 章并发会涉及到如何在多线程程序中进行引用计数。 ### 使用 `Rc` 共享数据 @@ -135,6 +135,6 @@ count after c goes out of scope = 2 从这个例子我们所不能看到的是,在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0,同时 `Rc` 被完全清理。使用 `Rc` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。 -通过不可变引用, `Rc` 允许在程序的多个部分之间只读地共享数据。如果 `Rc` 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 `RefCell` 类型,它可以与 `Rc` 结合使用来处理不可变性的限制。 +通过不可变引用, `Rc` 允许在程序的多个部分之间只读地共享数据。如果 `Rc` 也允许多个可变引用,则会违反第 4 章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 `RefCell` 类型,它可以与 `Rc` 结合使用来处理不可变性的限制。 [preventing-ref-cycles]: ch15-06-reference-cycles.html#preventing-reference-cycles-turning-an-rct-into-a-weakt diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index 8ac2b74..1f81ebb 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -1,12 +1,12 @@ ## `RefCell` 和内部可变性模式 -**内部可变性**(_Interior mutability_)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 +**内部可变性**(_Interior mutability_)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第 19 章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 让我们通过遵循内部可变性模式的 `RefCell` 类型来开始探索。 ### 通过 `RefCell` 在运行时检查借用规则 -不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第四章所学的借用规则: +不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第 4 章所学的借用规则: 1. 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 **之一**(而不是两者)。 2. 引用必须总是有效的。 @@ -19,7 +19,7 @@ 因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给开发者带来不便,但不会带来灾难。`RefCell` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 -类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell`,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用 `RefCell` 的功能。 +类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell`,会得到一个编译错误。第 16 章会介绍如何在多线程程序中使用 `RefCell` 的功能。 如下为选择 `Box`,`Rc` 或 `RefCell` 的理由: @@ -328,7 +328,7 @@ fn main() { 我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-18 一样。 -一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能([“`->` 运算符到哪去了?”][wheres-the---operator] 部分)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 +一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第 5 章讨论的自动解引用功能([“`->` 运算符到哪去了?”][wheres-the---operator] 部分)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 当我们打印出 `a`、`b` 和 `c` 时,可以看到他们都拥有修改后的值 15 而不是 5: diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index bcb4c8f..132ec64 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -24,7 +24,7 @@ Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编 ### 使用 `spawn` 创建新线程 -为了创建一个新线程,需要调用 `thread::spawn` 函数并传递一个闭包(第十三章学习了闭包),并在其中包含希望在新线程运行的代码。示例 16-1 中的例子在主线程打印了一些文本而另一些文本则由新线程打印: +为了创建一个新线程,需要调用 `thread::spawn` 函数并传递一个闭包(第 13 章学习了闭包),并在其中包含希望在新线程运行的代码。示例 16-1 中的例子在主线程打印了一些文本而另一些文本则由新线程打印: 文件名: src/main.rs @@ -165,9 +165,9 @@ hi number 4 from the main thread! ### 线程与 `move` 闭包 -`move` 闭包,我们曾在第十三章简要的提到过,其经常与 `thread::spawn` 一起使用,因为它允许我们在一个线程中使用另一个线程的数据。 +`move` 闭包,我们曾在第 13 章简要的提到过,其经常与 `thread::spawn` 一起使用,因为它允许我们在一个线程中使用另一个线程的数据。 -在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。 +在第 13 章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。 注意示例 16-1 中传递给 `thread::spawn` 的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。示例 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作,如下所示: @@ -232,7 +232,7 @@ fn main() { 示例 16-4: 一个具有闭包的线程,尝试使用一个在主线程中被回收的引用 `v` -假如这段代码能正常运行的话,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第十五章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了! +假如这段代码能正常运行的话,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第 15 章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了! 为了修复示例 16-3 的编译错误,我们可以听取错误信息的建议: diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index 4c7ff72..38d0964 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -24,7 +24,7 @@ fn main() { 这里使用 `mpsc::channel` 函数创建一个新的通道;`mpsc` 是 **多个生产者,单个消费者**(_multiple producer, single consumer_)的缩写。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 **发送**(_sending_)端,但只能有一个消费这些值的 **接收**(_receiving_)端。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。 -`mpsc::channel` 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(_transmitter_)和 **接收者**(_receiver_)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十八章会讨论 `let` 语句中的模式和解构。如此使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。 +`mpsc::channel` 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(_transmitter_)和 **接收者**(_receiver_)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第 18 章会讨论 `let` 语句中的模式和解构。如此使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。 让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了,如示例 16-7 所示。这类似于在河的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息: @@ -48,7 +48,7 @@ fn main() { 这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有通道的发送端以便能向通道发送消息。 -通道的发送端有一个 `send` 方法用来获取需要放入通道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。 +通道的发送端有一个 `send` 方法用来获取需要放入通道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第 9 章复习正确处理错误的策略。 在示例 16-8 中,我们在主线程中从通道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index 4e62de1..996afb8 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -6,7 +6,7 @@ > > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢? -在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 +在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第 15 章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 ### 互斥器一次只允许一个线程访问数据 @@ -107,11 +107,11 @@ in previous iteration of loop which does not implement the `Copy` trait ``` -错误信息表明 `counter` 值在上一次循环中被移动了。所以 Rust 告诉我们不能将 `counter` 锁的所有权移动到多个线程中。让我们通过一个第十五章讨论过的多所有权手段来修复这个编译错误。 +错误信息表明 `counter` 值在上一次循环中被移动了。所以 Rust 告诉我们不能将 `counter` 锁的所有权移动到多个线程中。让我们通过一个第 15 章讨论过的多所有权手段来修复这个编译错误。 #### 多线程和多所有权 -在第十五章中,通过使用智能指针 `Rc` 来创建引用计数的值,以便拥有多所有者。让我们在这也这么做看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中并在将所有权移入线程之前克隆了 `Rc`。现在我们理解了所发生的错误,同时也将代码改回使用 `for` 循环,并保留闭包的 `move` 关键字: +在第 15 章中,通过使用智能指针 `Rc` 来创建引用计数的值,以便拥有多所有者。让我们在这也这么做看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中并在将所有权移入线程之前克隆了 `Rc`。现在我们理解了所发生的错误,同时也将代码改回使用 `for` 循环,并保留闭包的 `move` 关键字: 文件名: src/main.rs @@ -214,8 +214,8 @@ Result: 10 ### `RefCell`/`Rc` 与 `Mutex`/`Arc` 的相似性 -你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第十五章中使用 `RefCell` 可以改变 `Rc` 中的内容那样,同样的可以使用 `Mutex` 来改变 `Arc` 中的内容。 +你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第 15 章中使用 `RefCell` 可以改变 `Rc` 中的内容那样,同样的可以使用 `Mutex` 来改变 `Arc` 中的内容。 -另一个值得注意的细节是 Rust 不能避免使用 `Mutex` 的全部逻辑错误。回忆一下第十五章使用 `Rc` 就有造成引用循环的风险,这时两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成 **死锁**(_deadlock_) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 +另一个值得注意的细节是 Rust 不能避免使用 `Mutex` 的全部逻辑错误。回忆一下第 15 章使用 `Rc` 就有造成引用循环的风险,这时两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成 **死锁**(_deadlock_) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait 以及如何对自定义类型使用他们。 diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index c2d6458..56bf493 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -10,25 +10,25 @@ Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 ** 因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 `Rc` 在线程间发送。当尝试在示例 16-14 中这么做的时候,会得到错误 `the trait Send is not implemented for Rc>`。而使用标记为 `Send` 的 `Arc` 时,就没有问题了。 -任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`。几乎所有基本类型都是 `Send` 的,除了第十九章将会讨论的裸指针(raw pointer)。 +任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`。几乎所有基本类型都是 `Send` 的,除了第 19 章将会讨论的裸指针(raw pointer)。 ### `Sync` 允许多线程访问 `Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T`(`T` 的引用)是 `Send` 的话 `T` 就是 `Sync` 的,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型是 `Sync` 的,完全由 `Sync` 的类型组成的类型也是 `Sync` 的。 -智能指针 `Rc` 也不是 `Sync` 的,出于其不是 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型不是 `Sync` 的。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 是 `Sync` 的,正如 [“在线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 +智能指针 `Rc` 也不是 `Sync` 的,出于其不是 `Send` 相同的原因。`RefCell`(第 15 章讨论过)和 `Cell` 系列类型不是 `Sync` 的。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 是 `Sync` 的,正如 [“在线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 ### 手动实现 `Send` 和 `Sync` 是不安全的 通常并不需要手动实现 `Send` 和 `Sync` trait,因为由 `Send` 和 `Sync` 的类型组成的类型,自动就是 `Send` 和 `Sync` 的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。 -手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[《Rust 秘典》][nomicon] 中有更多关于这些保证以及如何维持他们的信息。 +手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第 19 章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[《Rust 秘典》][nomicon] 中有更多关于这些保证以及如何维持他们的信息。 [nomicon]: https://doc.rust-lang.org/stable/nomicon/ ## 总结 -这不会是本书最后一个出现并发的章节:第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。 +这不会是本书最后一个出现并发的章节:第 20 章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。 正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index 928055e..3516f2e 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -1,6 +1,6 @@ ## 面向对象语言的特征 -关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。 +关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第 13 章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。 ### 对象包含数据和行为 @@ -18,7 +18,7 @@ 另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 -就像我们在第七章讨论的那样:可以使用 `pub` 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。比如,我们可以定义一个包含一个 `i32` 类型 vector 的结构体 `AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,`AveragedCollection` 会为我们缓存平均值结果。示例 17-1 有 `AveragedCollection` 结构体的定义: +就像我们在第 7 章讨论的那样:可以使用 `pub` 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。比如,我们可以定义一个包含一个 `i32` 类型 vector 的结构体 `AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,`AveragedCollection` 会为我们缓存平均值结果。示例 17-1 有 `AveragedCollection` 结构体的定义: 文件名: src/lib.rs diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md index 0604ca8..3dba54f 100644 --- a/src/ch17-02-trait-objects.md +++ b/src/ch17-02-trait-objects.md @@ -1,6 +1,6 @@ ## 为使用不同类型的值而设计的 trait 对象 -在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-10 中提供了一个定义 `SpreadsheetCell` 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。 +在第 8 章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-10 中提供了一个定义 `SpreadsheetCell` 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。 然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。在此之上,`gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个开发者可能会增加 `Image`,另一个可能会增加 `SelectBox`。 @@ -10,7 +10,7 @@ ### 定义通用行为的 trait -为了实现 `gui` 所期望的行为,让我们定义一个 `Draw` trait,其中包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的trait方法的表。我们通过指定某种指针来创建 trait 对象,例如 `&` 引用或 `Box` 智能指针,还有 `dyn` keyword, 以及指定相关的 trait(第十九章 [““动态大小类型和 `Sized` trait”][dynamically-sized] 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 +为了实现 `gui` 所期望的行为,让我们定义一个 `Draw` trait,其中包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的trait方法的表。我们通过指定某种指针来创建 trait 对象,例如 `&` 引用或 `Box` 智能指针,还有 `dyn` keyword, 以及指定相关的 trait(第 19 章 [““动态大小类型和 `Sized` trait”][dynamically-sized] 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 **则** 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。 @@ -26,7 +26,7 @@ pub trait Draw { 示例 17-3:`Draw` trait 的定义 -因为第十章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是新内容了:示例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 +因为第 10 章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是新内容了:示例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 文件名: src/lib.rs @@ -221,7 +221,7 @@ error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied ### trait 对象执行动态分发 -回忆一下第十章 [“泛型代码的性能”][performance-of-code-using-generics] 部分讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。 +回忆一下第 10 章 [“泛型代码的性能”][performance-of-code-using-generics] 部分讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。 当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5 和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。 diff --git a/src/ch17-03-oo-design-patterns.md b/src/ch17-03-oo-design-patterns.md index 429a4c0..0bb4be0 100644 --- a/src/ch17-03-oo-design-patterns.md +++ b/src/ch17-03-oo-design-patterns.md @@ -280,7 +280,7 @@ impl Post { 这里调用 `Option` 的 `as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option>`,调用 `as_ref` 会返回一个 `Option<&Box>`。如果不调用 `as_ref`,将会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。 -接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 +接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第 12 章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 接着我们就有了一个 `&Box`,当调用其 `content` 时,解引用强制转换会作用于 `&` 和 `Box` ,这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示: @@ -312,7 +312,7 @@ impl State for Published { 这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft` 和 `PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。 -注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。 +注意这个方法需要生命周期注解,如第 10 章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。 现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 @@ -334,7 +334,7 @@ impl State for Published { 另一个缺点是我们会发现一些重复的逻辑。为了消除他们,可以尝试为 `State` trait 中返回 `self` 的 `request_review` 和 `approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。 -另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第十九章的 [“宏”][macros] 部分)。 +另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第 19 章的 [“宏”][macros] 部分)。 完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。 diff --git a/src/ch18-00-patterns.md b/src/ch18-00-patterns.md index 8b703ed..34ca9e4 100644 --- a/src/ch18-00-patterns.md +++ b/src/ch18-00-patterns.md @@ -10,6 +10,6 @@ 这些部分描述了我们要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。 -我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。回忆一下第六章讨论 `match` 表达式时像硬币分类器那样使用模式。如果数据符合这个形状,就可以使用这些命名的片段。如果不符合,与该模式相关的代码则不会运行。 +我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。回忆一下第 6 章讨论 `match` 表达式时像硬币分类器那样使用模式。如果数据符合这个形状,就可以使用这些命名的片段。如果不符合,与该模式相关的代码则不会运行。 本章是所有模式相关内容的参考。我们将涉及到使用模式的有效位置,*refutable* 与 *irrefutable* 模式的区别,和你可能会见到的不同类型的模式语法。在最后,你将会看到如何使用模式创建强大而简洁的代码。 diff --git a/src/ch18-01-all-the-places-for-patterns.md b/src/ch18-01-all-the-places-for-patterns.md index f0bbcc7..0f19aa6 100644 --- a/src/ch18-01-all-the-places-for-patterns.md +++ b/src/ch18-01-all-the-places-for-patterns.md @@ -4,7 +4,7 @@ ### `match` 分支 -如第六章所讨论的,一个模式常用的位置是 `match` 表达式的分支。在形式上 `match` 表达式由 `match` 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式: +如第 6 章所讨论的,一个模式常用的位置是 `match` 表达式的分支。在形式上 `match` 表达式由 `match` 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式: ```text match VALUE { @@ -20,7 +20,7 @@ match VALUE { ### `if let` 条件表达式 -第六章讨论过了 `if let` 表达式,以及它是如何主要用于编写等同于只关心一个情况的 `match` 语句简写的。`if let` 可以对应一个可选的带有代码的 `else` 在 `if let` 中的模式不匹配时运行。 +第 6 章讨论过了 `if let` 表达式,以及它是如何主要用于编写等同于只关心一个情况的 `match` 语句简写的。`if let` 可以对应一个可选的带有代码的 `else` 在 `if let` 中的模式不匹配时运行。 示例 18-1 展示了也可以组合并匹配 `if let`、`else if` 和 `else if let` 表达式。这相比 `match` 表达式一次只能将一个值与模式比较提供了更多灵活性;一系列 `if let`、`else if`、`else if let` 分支并不要求其条件相互关联。 @@ -82,7 +82,7 @@ while let Some(top) = stack.pop() { ### `for` 循环 -如同第三章所讲的,`for` 循环是 Rust 中最常见的循环结构,不过还没有讲到的是 `for` 可以获取一个模式。在 `for` 循环中,模式是 `for` 关键字直接跟随的值,正如 `for x in y` 中的 `x`。 +如同第 3 章所讲的,`for` 循环是 Rust 中最常见的循环结构,不过还没有讲到的是 `for` 可以获取一个模式。在 `for` 循环中,模式是 `for` 关键字直接跟随的值,正如 `for x in y` 中的 `x`。 示例 18-3 中展示了如何使用 `for` 循环来解构,或拆开一个元组作为 `for` 循环的一部分: @@ -186,7 +186,7 @@ fn main() { 这会打印出 `Current location: (3, 5)`。值 `&(3, 5)` 会匹配模式 `&(x, y)`,如此 `x` 得到了值 `3`,而 `y`得到了值 `5`。 -因为如第十三章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。 +因为如第 13 章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。 现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工作;在一些地方,模式必须是 *irrefutable* 的,意味着他们必须匹配所提供的任何值。在另一些情况,他们则可以是 refutable 的。接下来让我们讨论这两个概念。 diff --git a/src/ch18-03-pattern-syntax.md b/src/ch18-03-pattern-syntax.md index 214ad9e..158dd86 100644 --- a/src/ch18-03-pattern-syntax.md +++ b/src/ch18-03-pattern-syntax.md @@ -4,7 +4,7 @@ ### 匹配字面值 -如第六章所示,可以直接匹配字面值模式。如下代码给出了一些例子: +如第 6 章所示,可以直接匹配字面值模式。如下代码给出了一些例子: ```rust let x = 1; @@ -184,7 +184,7 @@ fn main() { #### 解构枚举 -本书之前的部分曾经解构过枚举,比如第六章中示例 6-5 中解构了一个 `Option`。一个当时没有明确提到的细节是解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例 6-2 中的 `Message` 枚举为例,编写一个 `match` 使用模式解构每一个内部值,如示例 18-15 所示: +本书之前的部分曾经解构过枚举,比如第 6 章中示例 6-5 中解构了一个 `Option`。一个当时没有明确提到的细节是解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例 6-2 中的 `Message` 枚举为例,编写一个 `match` 使用模式解构每一个内部值,如示例 18-15 所示: 文件名: src/main.rs @@ -600,4 +600,4 @@ match msg { 模式是 Rust 中一个很有用的功能,它帮助我们区分不同类型的数据。当用于 `match` 语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。`let` 语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。 -接下来,在本书倒数第二章中,我们将介绍一些 Rust 众多功能中较为高级的部分。 +接下来,在本书倒数第 2 章中,我们将介绍一些 Rust 众多功能中较为高级的部分。 diff --git a/src/ch19-00-advanced-features.md b/src/ch19-00-advanced-features.md index 9854e41..f5ab035 100644 --- a/src/ch19-00-advanced-features.md +++ b/src/ch19-00-advanced-features.md @@ -1,6 +1,6 @@ # 高级特征 -现在我们已经学习了 Rust 编程语言中最常用的部分。在第二十章开始另一个新项目之前,让我们聊聊一些总有一天你会遇上的部分内容。你可以将本章作为不经意间遇到未知的内容时的参考。本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,我们希望确保你了解 Rust 提供的所有功能。 +现在我们已经学习了 Rust 编程语言中最常用的部分。在第 20 章开始另一个新项目之前,让我们聊聊一些总有一天你会遇上的部分内容。你可以将本章作为不经意间遇到未知的内容时的参考。本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,我们希望确保你了解 Rust 提供的所有功能。 本章将涉及如下内容: diff --git a/src/ch19-01-unsafe-rust.md b/src/ch19-01-unsafe-rust.md index 5c8b3af..5d4eeb9 100644 --- a/src/ch19-01-unsafe-rust.md +++ b/src/ch19-01-unsafe-rust.md @@ -30,7 +30,7 @@ ### 解引用裸指针 -回到第四章的 [“悬垂引用”][dangling-references] 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,**不可变** 意味着指针解引用之后不能直接赋值。 +回到第 4 章的 [“悬垂引用”][dangling-references] 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,**不可变** 意味着指针解引用之后不能直接赋值。 与引用和智能指针的区别在于,记住裸指针 @@ -210,7 +210,7 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 示例 19-6: 在 `split_at_mut` 函数的实现中使用不安全代码 -回忆第四章的 [“Slice 类型” ][the-slice-type] 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 +回忆第 4 章的 [“Slice 类型” ][the-slice-type] 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 我们保持索引 `mid` 位于 slice 中的断言。接着是不安全代码:`slice::from_raw_parts_mut` 函数获取一个裸指针和一个长度来创建一个 slice。这里使用此函数从 `ptr` 中创建了一个有 `mid` 个项的 slice。之后在 `ptr` 上调用 `add` 方法并使用 `mid` 作为参数来获取一个从 `mid` 开始的裸指针,使用这个裸指针并以 `mid` 之后项的数量为长度创建一个 slice。 @@ -294,7 +294,7 @@ fn main() { 静态变量类似于常量,我们在第 3 章的 "变量和常量的区别 "一节中讨论过。静态变量的名字按惯例采用SCREAMING_SNAKE_CASE。静态变量只能存储具有 "静态寿命 "的引用,这意味着Rust编译器可以计算出其寿命,我们不需要明确注释。访问一个不可变的静态变量是安全的。 -`static` 变量类似于第三章 [“变量和常量的区别”][differences-between-variables-and-constants] 部分讨论的常量。通常静态变量的名称采用 `SCREAMING_SNAKE_CASE` 写法。静态变量只能储存拥有 `'static` 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。 +`static` 变量类似于第 3 章 [“变量和常量的区别”][differences-between-variables-and-constants] 部分讨论的常量。通常静态变量的名称采用 `SCREAMING_SNAKE_CASE` 写法。静态变量只能储存拥有 `'static` 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。 常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。 @@ -324,7 +324,7 @@ fn main() { 就像常规变量一样,我们使用 `mut` 关键来指定可变性。任何读写 `COUNTER` 的代码都必须位于 `unsafe` 块中。这段代码可以编译并如期打印出 `COUNTER: 3`,因为这是单线程的。拥有多个线程访问 `COUNTER` 则可能导致数据竞争。 -拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用第十六章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。 +拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用第 16 章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。 ### 实现不安全 trait @@ -344,7 +344,7 @@ unsafe impl Foo for i32 { 通过 `unsafe impl`,我们承诺将保证编译器所不能验证的不变量。 -作为一个例子,回忆第十六章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait,编译器会自动为完全由 `Send` 和 `Sync` 类型组成的类型自动实现他们。如果实现了一个包含一些不是 `Send` 或 `Sync` 的类型,比如裸指针,并希望将此类型标记为 `Send` 或 `Sync`,则必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。 +作为一个例子,回忆第 16 章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait,编译器会自动为完全由 `Send` 和 `Sync` 类型组成的类型自动实现他们。如果实现了一个包含一些不是 `Send` 或 `Sync` 的类型,比如裸指针,并希望将此类型标记为 `Send` 或 `Sync`,则必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。 ### 访问联合体中的字段 diff --git a/src/ch19-03-advanced-traits.md b/src/ch19-03-advanced-traits.md index fb65fda..da67162 100644 --- a/src/ch19-03-advanced-traits.md +++ b/src/ch19-03-advanced-traits.md @@ -1,6 +1,6 @@ ## 高级 trait -第十章 [“trait:定义共享的行为”][traits-defining-shared-behavior] 部分,我们第一次涉及到了 trait,不过就像生命周期一样,我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。 +第 10 章 [“trait:定义共享的行为”][traits-defining-shared-behavior] 部分,我们第一次涉及到了 trait,不过就像生命周期一样,我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。 ### 关联类型在 trait 定义中指定占位符类型 @@ -8,7 +8,7 @@ 本章所描述的大部分内容都非常少见。关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。 -一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第十三章的 [“`Iterator` trait 和 `next` 方法”][the-iterator-trait-and-the-next-method] 部分曾提到过 `Iterator` trait 的定义如示例 19-12 所示: +一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第 13 章的 [“`Iterator` trait 和 `next` 方法”][the-iterator-trait-and-the-next-method] 部分曾提到过 `Iterator` trait 的定义如示例 19-12 所示: ```rust pub trait Iterator { @@ -24,7 +24,7 @@ pub trait Iterator { 关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢? -让我们通过一个在第十三章中出现的 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。在示例 13-21 中,指定了 `Item` 的类型为 `u32`: +让我们通过一个在第 13 章中出现的 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。在示例 13-21 中,指定了 `Item` 的类型为 `u32`: 文件名: src/lib.rs @@ -468,7 +468,7 @@ impl fmt::Display for Point { ### newtype 模式用以在外部类型上实现外部 trait -在第十章的 [“为类型实现 trait”][implementing-a-trait-on-a-type] 部分,我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 **newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第五章 [“用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。*Newtype* 是一个源自 ~~(U.C.0079,逃)~~ Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。 +在第 10 章的 [“为类型实现 trait”][implementing-a-trait-on-a-type] 部分,我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 **newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第 5 章 [“用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。*Newtype* 是一个源自 ~~(U.C.0079,逃)~~ Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。 例如,如果想要在 `Vec` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec` 都定义于我们的 crate 之外。可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体,接着可以如列表 19-31 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: @@ -495,7 +495,7 @@ fn main() { `Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 -此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。 +此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第 15 章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。 上面便是 newtype 模式如何与 trait 结合使用的;还有一个不涉及 trait 的实用模式。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。 diff --git a/src/ch19-04-advanced-types.md b/src/ch19-04-advanced-types.md index fd61169..7dbd989 100644 --- a/src/ch19-04-advanced-types.md +++ b/src/ch19-04-advanced-types.md @@ -10,7 +10,7 @@ newtype 模式可以用于一些其他我们还未讨论的功能,包括静态 另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能。 -newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap` 的 `People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十七章 [“封装隐藏了实现细节”][encapsulation-that-hides-implementation-details] 部分所讨论的隐藏实现细节的封装的轻量级方法。 +newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap` 的 `People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第 17 章 [“封装隐藏了实现细节”][encapsulation-that-hides-implementation-details] 部分所讨论的隐藏实现细节的封装的轻量级方法。 ### 类型别名用来创建类型同义词 @@ -139,7 +139,7 @@ let guess: u32 = match guess.trim().parse() { 示例 19-26: `match` 语句和一个以 `continue` 结束的分支 -当时我们忽略了代码中的一些细节。在第六章 [“`match` 控制流运算符”][the-match-control-flow-operator] 部分,我们学习了 `match` 的分支必须返回相同的类型。如下代码不能工作: +当时我们忽略了代码中的一些细节。在第 6 章 [“`match` 控制流运算符”][the-match-control-flow-operator] 部分,我们学习了 `match` 的分支必须返回相同的类型。如下代码不能工作: ```rust,ignore,does_not_compile let guess = match guess.trim().parse() { @@ -194,11 +194,11 @@ let s2: str = "How's it going?"; Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。 -那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分,slice 数据结储存了开始位置和 slice 的长度。 +那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第 4 章 [“字符串 slice”][string-slices] 部分,slice 数据结储存了开始位置和 slice 的长度。 所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。 -可以将 `str` 与所有类型的指针结合:比如 `Box` 或 `Rc`。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&dyn Trait` 或 `Box`(`Rc` 也可以)。 +可以将 `str` 与所有类型的指针结合:比如 `Box` 或 `Rc`。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第 17 章 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&dyn Trait` 或 `Box`(`Rc` 也可以)。 为了处理 DST,Rust 有一个特定的 trait 来确定一个类型的大小是否在编译时可知:这就是 `Sized` trait。这个 trait 自动为编译器在编译时就知道其大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说,对于如下泛型函数定义: diff --git a/src/ch19-05-advanced-functions-and-closures.md b/src/ch19-05-advanced-functions-and-closures.md index 6e1e120..cfa0660 100644 --- a/src/ch19-05-advanced-functions-and-closures.md +++ b/src/ch19-05-advanced-functions-and-closures.md @@ -108,7 +108,7 @@ fn returns_closure() -> Box i32> { } ``` -这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十七章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分。 +这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第 17 章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分。 接下来让我们学习宏! diff --git a/src/ch19-06-macros.md b/src/ch19-06-macros.md index 4fddb58..a66180b 100644 --- a/src/ch19-06-macros.md +++ b/src/ch19-06-macros.md @@ -22,9 +22,9 @@ ### 使用 `macro_rules!` 的声明宏用于通用元编程 -Rust 最常用的宏形式是 **声明宏**(*declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第六章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。 +Rust 最常用的宏形式是 **声明宏**(*declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第 6 章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。 -可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第八章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector: +可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第 8 章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector: ```rust let v: Vec = vec![1, 2, 3]; @@ -62,7 +62,7 @@ macro_rules! vec { `vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。 -宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。 +宏定义中有效模式语法和在第 18 章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。 [参考]: https://doc.rust-lang.org/reference/macros.html diff --git a/src/ch20-01-single-threaded.md b/src/ch20-01-single-threaded.md index 73c93f2..6dfad6f 100644 --- a/src/ch20-01-single-threaded.md +++ b/src/ch20-01-single-threaded.md @@ -173,7 +173,7 @@ HTTP/1.1 200 OK\r\n\r\n 示例 20-5: 将 *hello.html* 的内容作为响应 body 发送 -在开头增加了一行来将标准库中的 `File` 引入作用域。打开和读取文件的代码应该看起来很熟悉,因为第十二章 I/O 项目的示例 12-4 中读取文件内容时出现过类似的代码。 +在开头增加了一行来将标准库中的 `File` 引入作用域。打开和读取文件的代码应该看起来很熟悉,因为第 12 章 I/O 项目的示例 12-4 中读取文件内容时出现过类似的代码。 接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。 @@ -233,7 +233,7 @@ HTTP/1.1 200 OK\r\n\r\n 示例 20-9: 重构使得 `if` 和 `else` 块中只包含两个情况所不同的代码 -现在 `if` 和 `else` 块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用第十八章讲到的使用模式的 `let` 语句通过解构元组的两部分为 `filename` 和 `header` 赋值。 +现在 `if` 和 `else` 块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用第 18 章讲到的使用模式的 `let` 语句通过解构元组的两部分为 `filename` 和 `header` 赋值。 之前读取文件和写入响应的冗余代码现在位于 `if` 和 `else` 块之外,并会使用变量 `status_line` 和 `filename`。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例 20-9 中代码的行为与示例 20-8 完全一样。 diff --git a/src/ch20-02-multithreaded.md b/src/ch20-02-multithreaded.md index f2da409..88111e0 100644 --- a/src/ch20-02-multithreaded.md +++ b/src/ch20-02-multithreaded.md @@ -59,7 +59,7 @@ fn handle_connection(mut stream: TcpStream) { 在开始之前,让我们讨论一下线程池应用看起来怎样。当尝试设计代码时,首先编写客户端接口确实有助于指导代码设计。以期望的调用方式来构建 API 代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API。 -类似于第十二章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(compiler-driven development)。我们将编写调用所期望的函数的代码,接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。 +类似于第 12 章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(compiler-driven development)。我们将编写调用所期望的函数的代码,接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。 #### 为每一个请求分配线程的代码结构 @@ -89,7 +89,7 @@ fn main() { 示例 20-11: 为每一个流新建一个线程 -正如第十六章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 */sleep*,接着在另两个浏览器标签页中加载 */*,确实会发现 */* 请求不必等待 */sleep* 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。 +正如第 16 章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 */sleep*,接着在另两个浏览器标签页中加载 */*,确实会发现 */* 请求不必等待 */sleep* 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。 #### 为有限数量的线程创建一个类似的接口 @@ -191,7 +191,7 @@ impl ThreadPool { } ``` -这里选择 `usize` 作为 `size` 参数的类型,因为我们知道为负的线程数没有意义。我们还知道将使用 4 作为线程集合的元素数量,这也就是使用 `usize` 类型的原因,如第三章 [“整数类型”][integer-types] 部分所讲。 +这里选择 `usize` 作为 `size` 参数的类型,因为我们知道为负的线程数没有意义。我们还知道将使用 4 作为线程集合的元素数量,这也就是使用 `usize` 类型的原因,如第 3 章 [“整数类型”][integer-types] 部分所讲。 再次编译检查这段代码: @@ -216,7 +216,7 @@ error[E0599]: no method named `execute` found for type `hello::ThreadPool` in th 现在有了一个警告和一个错误。暂时先忽略警告,发生错误是因为并没有 `ThreadPool` 上的 `execute` 方法。回忆 [“为有限数量的线程创建一个类似的接口”](#creating-a-similar-interface-for-a-finite-number-of-threads) 部分我们决定线程池应该有与 `thread::spawn` 类似的接口,同时我们将实现 `execute` 函数来获取传递的闭包并将其传递给池中的空闲线程执行。 -我们会在 `ThreadPool` 上定义 `execute` 函数来获取一个闭包参数。回忆第十三章的 [“使用带有泛型和 `Fn` trait 的闭包”][storing-closures-using-generic-parameters-and-the-fn-traits] 部分,闭包作为参数时可以使用三个不同的 trait:`Fn`、`FnMut` 和 `FnOnce`。我们需要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 `thread::spawn`,所以我们可以观察 `thread::spawn` 的签名在其参数中使用了何种 bound。查看文档会发现: +我们会在 `ThreadPool` 上定义 `execute` 函数来获取一个闭包参数。回忆第 13 章的 [“使用带有泛型和 `Fn` trait 的闭包”][storing-closures-using-generic-parameters-and-the-fn-traits] 部分,闭包作为参数时可以使用三个不同的 trait:`Fn`、`FnMut` 和 `FnOnce`。我们需要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 `thread::spawn`,所以我们可以观察 `thread::spawn` 的签名在其参数中使用了何种 bound。查看文档会发现: ```rust,ignore pub fn spawn(f: F) -> JoinHandle @@ -302,7 +302,7 @@ impl ThreadPool { 示例 20-13: 实现 `ThreadPool::new` 在 `size` 为零时 panic -这里用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何! +这里用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第 14 章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何! 相比像这里使用 `assert!` 宏,也可以让 `new` 像之前 I/O 项目中示例 12-9 中 `Config::new` 那样返回一个 `Result`,不过在这里我们选择创建一个没有任何线程的线程池应该是不可恢复的错误。如果你想做的更好,尝试编写一个采用如下签名的 `new` 版本来感受一下两者的区别: @@ -439,7 +439,7 @@ impl Worker { 我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行他们。 -在第十六章,我们学习了 **通道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里通道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: +在第 16 章,我们学习了 **通道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里通道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: 1. `ThreadPool` 会创建一个通道并充当发送端。 2. 每个 `Worker` 将会充当通道的接收端。 @@ -567,11 +567,11 @@ error[E0382]: use of moved value: `receiver` `std::sync::mpsc::Receiver`, which does not implement the `Copy` trait ``` -这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的,回忆第十六章:Rust 所提供的通道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆通道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。 +这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的,回忆第 16 章:Rust 所提供的通道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆通道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。 -另外,从通道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。 +另外,从通道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第 16 章)。 -回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改: +回忆一下第 16 章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改: 文件名: src/lib.rs @@ -640,7 +640,7 @@ impl Worker { #### 实现 `execute` 方法 -最后让我们实现 `ThreadPool` 上的 `execute` 方法。同时也要修改 `Job` 结构体:它将不再是结构体,`Job` 将是一个有着 `execute` 接收到的闭包类型的 trait 对象的类型别名。第十九章 [“类型别名用来创建类型同义词”][creating-type-synonyms-with-type-aliases] 部分提到过,类型别名允许将长的类型变短。观察示例 20-19: +最后让我们实现 `ThreadPool` 上的 `execute` 方法。同时也要修改 `Job` 结构体:它将不再是结构体,`Job` 将是一个有着 `execute` 接收到的闭包类型的 trait 对象的类型别名。第 19 章 [“类型别名用来创建类型同义词”][creating-type-synonyms-with-type-aliases] 部分提到过,类型别名允许将长的类型变短。观察示例 20-19: 文件名: src/lib.rs @@ -757,7 +757,7 @@ Worker 2 got a job; executing. > 注意如果同时在多个浏览器窗口打开 */sleep*,它们可能会彼此间隔地加载 5 秒,因为一些浏览器处于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 web server 造成的。 -在学习了第十八章的 `while let` 循环之后,你可能会好奇为何不能如此编写 worker 线程,如示例 20-21 所示: +在学习了第 18 章的 `while let` 循环之后,你可能会好奇为何不能如此编写 worker 线程,如示例 20-21 所示: 文件名: src/lib.rs diff --git a/src/ch20-03-graceful-shutdown-and-cleanup.md b/src/ch20-03-graceful-shutdown-and-cleanup.md index d361695..2373f57 100644 --- a/src/ch20-03-graceful-shutdown-and-cleanup.md +++ b/src/ch20-03-graceful-shutdown-and-cleanup.md @@ -109,7 +109,7 @@ impl Drop for ThreadPool { } ``` -如第十七章我们见过的,`Option` 上的 `take` 方法会取出 `Some` 而留下 `None`。使用 `if let` 解构 `Some` 并得到线程,接着在线程上调用 `join`。如果 worker 的线程已然是 `None`,就知道此时这个 worker 已经清理了其线程所以无需做任何操作。 +如第 17 章我们见过的,`Option` 上的 `take` 方法会取出 `Some` 而留下 `None`。使用 `if let` 解构 `Some` 并得到线程,接着在线程上调用 `join`。如果 worker 的线程已然是 `None`,就知道此时这个 worker 已经清理了其线程所以无需做任何操作。 ### 向线程发送信号使其停止接收任务