mirror of
https://github.com/rust-lang-cn/book-cn.git
synced 2025-01-23 07:20:20 +08:00
Update ch03 and some words
This commit is contained in:
parent
2a8bac71f2
commit
330ac55e5e
@ -28,7 +28,7 @@
|
||||
|
||||
[nostarch]: https://nostarch.com/rust
|
||||
|
||||
在线上可以本组织官网[阅读中文版][book-cn](**支持同一页面中英双语切换**)或在 Rust 官网上[阅读英文版][book-en]。另外在 Rust 官网的英文版在线阅读可以选择 [stable]、[beta] 和 [nightly] 三个不同的分支版本,这几个分支版本可能滞后于[英文仓库][github-en]的最新内容。
|
||||
在线版可以在本组织官网上[阅读中文版][book-cn](**支持同一页面中英双语切换**)或在 Rust 官网上[阅读英文版][book-en]。另外在 Rust 官网的英文版在线阅读可以选择 [stable]、[beta] 和 [nightly] 三个不同的分支版本,这几个分支版本可能滞后于[英文仓库][github-en]的最新内容。
|
||||
|
||||
[book-cn]: https://rustwiki.org/zh-CN/book
|
||||
[book-en]: https://doc.rust-lang.org/book/
|
||||
|
@ -48,7 +48,7 @@
|
||||
|
||||
例如,对于来自于 `rand` crate 中的 `gen_range` 方法来说,当在一个大值和小值指定的范围内生成一个随机值时,`PartialOrd` trait 是必须的。
|
||||
|
||||
`Ord` trait 也让你明白在一个带注解类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option<Ordering>`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd` 和 `Eq`(`Eq` 依赖 `PartialEq`)的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。
|
||||
`Ord` trait 也让你明白在一个带注明类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option<Ordering>`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd` 和 `Eq`(`Eq` 依赖 `PartialEq`)的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。
|
||||
|
||||
例如,当在 `BTreeSet<T>`(一种基于有序值存储数据的数据结构)上存值时,`Ord` 是必须的。
|
||||
|
||||
|
@ -79,7 +79,7 @@ $ cd guessing_game
|
||||
|
||||
这些代码仅仅打印提示,介绍游戏的内容然后请求用户输入。
|
||||
|
||||
### 使用变量储存值
|
||||
### 使用变量存储值
|
||||
|
||||
接下来,创建一个储存用户输入的**变量**(*variable*),像这样:
|
||||
|
||||
@ -408,7 +408,7 @@ let guess: u32 = guess.trim().parse().expect("Please type a number!");
|
||||
我们将这个新变量绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 是指原始的 `guess` 变量,其中包含作为字符串的输入。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符,我们必须执行此方法才能将字符串与 `u32` 比较,因为 `u32` 只能包含数值型数据。用户必须输入 <span class="keystroke">enter</span> 键才能让 `read_line` 返回,并输入他们的猜想,这会在字符串中增加一个换行符。例如,用户输入 <span class="keystroke">5</span> 并按下 <span class="keystroke">enter</span>,`guess` 看起来像这样:`5\n`,`\n` 代表 “换行”(在 Windows 中,按 <span
|
||||
class="keystroke">enter</span> 键会得到一个回车和一个换行符 `\r\n`)。`trim` 方法会消除 `\n` 或 `\r\n`,只留下 `5`。
|
||||
|
||||
[字符串的 `parse` 方法][parse]<!-- ignore --> 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 `let guess: u32` 指定。`guess` 后面的冒号(`:`)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;`u32` 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第 3 章还会讲到其他数字类型。另外,程序中的 `u32` 注解以及与 `secret_number` 的比较,意味着 Rust 会推断出 `secret_number` 也是 `u32` 类型。现在可以使用相同类型比较两个值了!
|
||||
[字符串的 `parse` 方法][parse]<!-- ignore --> 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 `let guess: u32` 指定。`guess` 后面的冒号(`:`)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;`u32` 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第 3 章还会讲到其他数字类型。另外,程序中的 `u32` 标注以及与 `secret_number` 的比较,意味着 Rust 会推断出 `secret_number` 也是 `u32` 类型。现在可以使用相同类型比较两个值了!
|
||||
|
||||
由于 `parse` 方法只能用于可以逻辑转换为数字的字符,所以调用它很容易产生错误。例如,字符串中包含 `A👍%`,就无法将其转换为一个数字。因此,`parse` 方法返回一个 `Result` 类型。像前面 [“使用 `Result` 类型来处理潜在的错误”][handling-potential-failure-with-the-result-type]<!-- ignore--> 部分讨论的 `read_line` 方法那样,再次按部就班地用 `expect` 方法处理即可。如果 `parse` 不能从字符串生成一个数字,返回一个 `Result` 的 `Err` 成员时,`expect` 会使游戏崩溃并打印附带的信息。如果 `parse` 成功地将字符串转换为一个数字,它会返回 `Result` 的 `Ok` 成员,然后 `expect` 会返回 `Ok` 值中的数字。
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
具体而言,你将学习到变量、基本类型、函数、注释和控制流。这些基本内容几乎在每个 Rust 程序中都会遇到,所以尽早接触这些内容,从核心知识学起。
|
||||
|
||||
> ### 关键字
|
||||
> #### 关键字
|
||||
>
|
||||
> Rust 语言有一系列**关键字**(*keywords*),和其他语言一样,这些关键字被保留下来只提供给语言作特殊使用。请记住,这些关键字不能用作变量或函数的名称。大多数关键字都有特殊的含义,在 Rust 程序中可使用它们执行多种任务;有少数关键字暂时没有相关联的功能,但这些功能将来可能会加入到 Rust 中。在[附录 A][appendix_a]<!-- ignore --> 中可找到关键字列表。
|
||||
> Rust 语言有一系列**关键字**(*keywords*),和其他语言一样,这些关键字被保留下来只提供给语言作特殊使用。请记住,这些关键字不能用作变量或函数的名称。大多数关键字都有特殊的含义,在 Rust 程序中可使用它们执行多种任务;有少数关键字暂时没有相关联的功能,但新功能将来可能会加入到 Rust 中。在[附录 A][appendix_a]<!-- ignore --> 中可找到关键字列表。
|
||||
|
||||
[appendix_a]: appendix-01-keywords.md
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 变量和可变性
|
||||
|
||||
如第 2 章所提到的,默认情况下变量是**不可变的**(*immutable*)。这是 Rust 中众多精妙之处的其中一个,Rust 的这些设计点鼓励你以一种充分利用 Rust 提供的安全和简单并发的方式来编写代码。不过你也可以选择让变量是**可变的**(*mutable*)。让我们来探讨为什么 Rust 鼓励你选用不可变性,以及为什么你可能不喜欢这样。
|
||||
如[“使用变量存储值”][storing-values-with-variables]<!-- ignore -->章节所述,默认情况下变量是**不可变的**(*immutable*)。这是 Rust 众多精妙之处的其中一个,这些特性让你充分利用 Rust 提供的安全性和简单并发性的方式来编写代码。不过你也可以选择让变量是**可变的**(*mutable*)。让我们探讨一下 Rust 如何及为什么鼓励你选用不可变性,以及为什么有时你可能不选用。
|
||||
|
||||
当变量不可变时,这意味着一旦一个值绑定到一个变量名后,就不能再更改该值了。为了说明,我们在 *projects* 目录下使用 `cargo new variables` 来创建一个名为 *variables* 新项目。
|
||||
当变量不可变时,这意味着一旦一个值绑定到一个变量名后,就不能更改该值了。为了说明,我们在 *projects* 目录下使用 `cargo new variables` 来创建一个名为 *variables* 新项目。
|
||||
|
||||
然后在新建的 *variables* 目录下,打开 *src/main.rs* 并将代码替换为下面还未能通过编译的代码:
|
||||
然后在新建的 *variables* 目录下,打开 *src/main.rs* 并将代码替换成以下代码。这段代码还不能编译,我们先检查不可变错误(immutability error):
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -12,22 +12,20 @@
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/src/main.rs}}
|
||||
```
|
||||
|
||||
保存文件,并使用 `cargo run` 运行程序。你将会收到一条错误消息,输出如下所示:
|
||||
保存文件,并使用 `cargo run` 运行程序。你将会收到一条错误信息,输出如下所示:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/output.txt}}
|
||||
```
|
||||
|
||||
这个例子展示了编译器如何帮助你查找程序中的错误。即使编译器错误可能令人沮丧,它们也只是表明你的程序做你想做的事情并不安全;并**不**意味着你不是一个好开发者!有经验的 Rustaceans 依然会遇到编译错误。
|
||||
这个例子展示了编译器如何帮助你查找程序中的错误。编译器错误可能令人沮丧,但它们也只是表明你的程序做你想做的事情并不安全;并**不**意味着你不是一个好开发者!有经验的 Rustacean(Rust 开发者) 依然会遇到编译错误。
|
||||
|
||||
上面的错误指出错误的原因是 `cannot
|
||||
assign twice to immutable variable x`(不能对不可变变量二次赋值),因为我们尝试给不可变的 `x` 变量赋值为第二个值。
|
||||
上面的错误指出错误的原因是 `` cannot
|
||||
assign twice to immutable variable `x` ``(不能对不可变变量二次赋值),因为我们尝试给不可变的 `x` 变量赋值为第二个值。
|
||||
|
||||
当我们尝试改变一个前面指定为不可变的值时我们会得到编译期错误,这点很重要,因为这种情况很可能导致 bug。如果我们代码的一部分假设某个值永远不会更改,而代码的另一部分更改了该值,那很可能第一部分代码不会按照所设计的逻辑运行。这个 bug 的根源在实际开发中可能很难追踪,特别是第二部分代码只是**偶尔**变更了原来的值。
|
||||
当我们尝试改变一个前面指定为不可变的值时我们会得到编译期错误,这点很重要,因为这种情况很可能导致 bug。如果我们代码的一部分假设某个值永远不会更改,而代码的另一部分更改了该值,那很可能第一部分代码以不可意料的方式运行。这个 bug 的根源在实际开发中可能很难追踪,特别是第二部分代码只是**偶尔**变更了原来的值。Rust 编译器保证了当我们声明了一个值不会改变时,那它就真的不可改变,所以你不必亲自跟踪这个值。这可以使得代码更容易理解。
|
||||
|
||||
在 Rust 中,编译器保证了当我们声明了一个值不会改变,那它就真的不可改变。这意味着当你正在阅读和编写代码时,就不必跟踪一个值怎样变化以及在哪发生改变,这可以使得代码更容易理解。
|
||||
|
||||
但可变性有时也相当重要。变量只是默认不可变的;我们可以通过在变量名前加上 `mut` 使得它们可变。除了允许这个值改变外,它还向以后的读代码的人传达了这样的意思:代码的其他部分将会改变这个变量值。
|
||||
但可变性有时也相当重要,可使代码更方便编写。变量只是默认不可变的;像第 2 章所做的那样,我们可以通过在变量名前加上 `mut` 使得它们可变。增加 `mut` 的操作还向以后的读代码的人传达了代码的其他部分将会改变这个变量值。
|
||||
|
||||
例如将 *src/main.rs* 改为以下内容:
|
||||
|
||||
@ -43,36 +41,33 @@ assign twice to immutable variable x`(不能对不可变变量二次赋值)
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-02-adding-mut/output.txt}}
|
||||
```
|
||||
|
||||
加上 `mut` 后,我们就可以将 `x` 绑定的值从 `5` 改成 `6`。在一些情况下,你需要变量是可变的,因为相比只使用不可变变量的实现,这可使得代码更容易编写。
|
||||
加上 `mut` 后,我们就可以将 `x` 绑定的值从 `5` 改成 `6`。除了预防 bug 外,还有很多权衡要取舍。例如,在使用大型数据结构的情形下,在同一位置更改实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式编程的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。
|
||||
|
||||
除了预防 bug 外,还有很多权衡要考虑。例如,在使用大型数据结构的情形下,在同一位置更改实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。
|
||||
### 常量
|
||||
|
||||
### 变量和常量之间的差异
|
||||
与不可变变量类似,**常量**(*constant*)是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异。
|
||||
|
||||
变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:**常量**(*constant*)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异。
|
||||
|
||||
首先,常量不允许使用 `mut`。常量不仅仅默认不可变,而且自始至终不可变。
|
||||
|
||||
常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。我们将在下一节[“数据类型”][data-types]<!-- ignore
|
||||
-->中介绍类型和类型标注,因此现在暂时不需关心细节。只需知道你必须始终对类型进行标注。
|
||||
首先,常量不允许使用 `mut`。常量不仅仅默认不可变,而且自始至终不可变。常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**注明。我们将在下一节[“数据类型”][data-types]<!-- ignore -->中介绍类型和类型标注,现在暂时不需关心细节。只需知道你必须始终对类型进行注明。
|
||||
|
||||
常量可以在任意作用域内声明,包括全局作用域,这对于代码中很多部分都需要知道一个值的情况特别有用。
|
||||
|
||||
最后一个不同点是常量只能设置为常量表达式,而不能是函数调用的结果或是只能在运行时计算得到的值。
|
||||
|
||||
下面是一个常量声明的例子,其常量名为 `MAX_POINTS`,值设置为 100,000。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性):
|
||||
下面是一个常量声明的例子:
|
||||
|
||||
```rust
|
||||
const MAX_POINTS: u32 = 100_000;
|
||||
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
|
||||
```
|
||||
|
||||
在声明的作用域内,常量在程序运行的整个过程中都有效。对于应用程序域中程序的多个部分可能都需要知道的值的时候,常量是一个很有用的选择,例如游戏中允许玩家赚取的最大点数或光速。
|
||||
这个常量名为 `THREE_HOURS_IN_SECONDS`,值设置为 60(一分钟内的秒数)乘以 60(一小时内分钟数)再乘以 3(我们在这程序)。Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词。编译器能够在编译时计算一些有限的操作,这让我们可以选择以更容易理解和验证的方式写出这个值,而不是将此常量设置为值 10,800。有关在声明常量时可以使用哪些操作的更多信息,请参阅[《Rust 参考手册》中关于常量计算的章节][const-eval]。
|
||||
|
||||
在声明的作用域内,常量在程序运行的整个过程中都有效。对于应用程序域中程序的多个部分可能都需要知道的值的时候,常量是一个很有用的选择,例如游戏中允许玩家赚取的最高分或光速。
|
||||
|
||||
将整个程序中用到的硬编码(hardcode)值命名为常量,对于将该值的含义传达给代码的未来维护者很有用。如果将来需要更改硬编码的值,则只需要在代码中改动一处就可以了。
|
||||
|
||||
### 变量遮蔽
|
||||
### 遮蔽
|
||||
|
||||
正如你在第 2 章[“猜数字游戏”][comparing-the-guess-to-the-secret-number]<!-- ignore -->章节中所看到的,你可以声明和前面变量具有相同名称的新变量。Rustaceans 说这个是第一个变量被第二个变量**遮蔽**(*shadow*),这意味着当我们使用变量时我们看到的会是第二个变量的值。我们可以通过使用相同的变量名并重复使用 `let` 关键字来遮蔽变量,如下所示:
|
||||
正如你在第 2 章[“猜数字游戏”][comparing-the-guess-to-the-secret-number]<!-- ignore -->章节中所看到的,你可以声明和前面变量具有相同名称的新变量。Rustacean 说这个是第一个变量被第二个变量**遮蔽**(*shadow*),这意味着当我们使用变量时我们看到的会是第二个变量的值。我们可以通过使用相同的变量名并重复使用 `let` 关键字来遮蔽变量,如下所示:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -80,21 +75,21 @@ const MAX_POINTS: u32 = 100_000;
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-03-shadowing/src/main.rs}}
|
||||
```
|
||||
|
||||
这个程序首先将数值 `5` 绑定到 `x`。然后通过重复使用 `let x =` 来遮蔽之前的 `x`,并取原来的值加上 `1`,所以 `x` 的值变成了 `6`。第三个 `let` 语句同样遮蔽前面的 `x`,取之前的值并乘上 `2`,得到的 `x` 最终值为 `12`。当运行此程序,将输出以下内容:
|
||||
这个程序首先将数值 `5` 绑定到 `x`。然后通过重复使用 `let x =` 来遮蔽之前的 `x`,并取原来的值加上 `1`,所以 `x` 的值变成了 `6`。在内部作用域内,第三个 `let` 语句同样遮蔽前面的 `x`,取之前的值并乘上 `2`,得到的 `x` 值为 `12`。当该作用域结束时,内部遮蔽结束并且 `x` 恢复成 `6`。当运行此程序,将输出以下内容:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-03-shadowing/output.txt}}
|
||||
```
|
||||
|
||||
这和将变量标记为 `mut` 的方式不同,因为除非我们再次使用 `let` 关键字,否则若是我们不小心尝试重新赋值给这个变量,我们将得到一个编译错误。通过使用 `let`,我们可以对一个值进行一些转换,但在这些转换完成后,变量将是不可变的。
|
||||
遮蔽和将变量标记为 `mut` 的方式不同,因为除非我们再次使用 `let` 关键字,否则若是我们不小心尝试重新赋值给这个变量,我们将得到一个编译错误。通过使用 `let`,我们可以对一个值进行一些转换,但在这些转换完成后,变量将是不可变的。
|
||||
|
||||
`mut` 和变量遮蔽之间的另一个区别是,因为我们在再次使用 `let` 关键字时有效地创建了一个新的变量,所以我们可以改变值的类型,但重复使用相同的名称。例如,假设我们程序要求用户输入空格字符来显示他们想要的空格数目,但我们实际上想要将该输入存储为一个数字:
|
||||
`mut` 和遮蔽之间的另一个区别是,因为我们在再次使用 `let` 关键字时有效地创建了一个新的变量,所以我们可以改变值的类型,但重复使用相同的名称。例如,假设我们程序要求用户输入空格字符来显示他们想要的空格数目,但我们实际上想要将该输入存储为一个数字:
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-04-shadowing-can-change-types/src/main.rs:here}}
|
||||
```
|
||||
|
||||
这种结构是允许的,因为第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个全新的变量且和第第一个具有相同的变量名,且是一个数字类型。所以变量遮蔽可以让我们就不必给出不同的名称,如 `spaces_str` 和 `spaces_num`;相反我们可以重复使用更简单的 `spaces` 变量名。然而,如果我们对此尝试使用 `mut`,如下所示,我们将得到一个编译期错误:
|
||||
第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个数字类型。所以变量遮蔽可以让我们不必给出不同的名称,如 `spaces_str` 和 `spaces_num`,相反我们可以重复使用更简单的 `spaces` 变量名。然而,如果我们对此尝试使用 `mut`,如下所示,我们将得到一个编译期错误:
|
||||
|
||||
```rust,ignore,does_not_compile
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-05-mut-cant-change-types/src/main.rs:here}}
|
||||
@ -106,8 +101,9 @@ const MAX_POINTS: u32 = 100_000;
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-05-mut-cant-change-types/output.txt}}
|
||||
```
|
||||
|
||||
现在我们已经探索了变量是如何工作的,接下来我们学习更多的数据类型。
|
||||
现在我们已经探讨了变量是如何工作的,接下来我们学习更多的数据类型。
|
||||
|
||||
[comparing-the-guess-to-the-secret-number]:
|
||||
ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字
|
||||
[comparing-the-guess-to-the-secret-number]: ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字
|
||||
[data-types]: ch03-02-data-types.html#数据类型
|
||||
[storing-values-with-variables]: ch02-00-guessing-game-tutorial.html#使用变量存储值
|
||||
[const-eval]: https://rustwiki.org/zh-CN/reference/const_eval.html
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Rust 的每个值都有确切的**数据类型**(*data type*),该类型告诉 Rust 数据是被指定成哪类数据,从而让 Rust 知道如何使用该数据。在本节中,我们将介绍两种数据类型:标量类型和复合类型。
|
||||
|
||||
请记住 Rust 是一种**静态类型**(*statically typed*)的语言,这意味着它必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式推导出我们想要使用的类型。在类型可能是多种情况时,例如在第 2 章的[“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number]<!-- ignore -->中当我们使用 `parse` 将`String` 转换成数值类型时,我们必须加上一个类型标注,如下所示:
|
||||
请记住 Rust 是一种**静态类型**(*statically typed*)的语言,这意味着它必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式推导出我们想要使用的类型。在类型可能是多种情况时,例如在第 2 章[“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number]<!-- ignore -->中当我们使用 `parse` 将`String` 转换成数值类型时,我们必须加上一个类型标注,如下所示:
|
||||
|
||||
```rust
|
||||
let guess: u32 = "42".parse().expect("Not a number!");
|
||||
@ -18,13 +18,13 @@ let guess: u32 = "42".parse().expect("Not a number!");
|
||||
|
||||
### 标量类型
|
||||
|
||||
**标量**(*scalar*)类型表示单个值。Rust 有 4 个标量类型:整型、浮点型、布尔型和字符。你可能从其他语言了解过这些类型。下面我们深入了解它们在 Rust 中的用法。
|
||||
**标量**(*scalar*)类型表示单个值。Rust 有 4 个基本的标量类型:整型、浮点型、布尔型和字符。你可能从其他语言了解过这些类型。下面我们深入了解它们在 Rust 中的用法。
|
||||
|
||||
#### 整数类型
|
||||
|
||||
**整数**(*integer*)是没有小数部分的数字。我们在第 2 章使用过一个整数类型,即 `u32` 类型。此类型声明表明了与其相关的值应为 32 位系统的无符号整型(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。表 3-1 显示了 Rust 中的内置的整数类型。在有符号和和无符号的列中(例如 *i16*)的每个定义形式都可用于声明整数类型。
|
||||
**整数**(*integer*)是没有小数部分的数字。我们在第 2 章使用过一个整数类型(整型),即 `u32` 类型。此类型声明表明它关联的值应该是占用 32 位空间的无符号整型(有符号整型以 `i` 开始,`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。表 3-1 显示了 Rust 中的内置的整数类型。我们可以使用这些定义形式中的任何一个来声明整数值的类型。
|
||||
|
||||
<span class="caption">表 3-1: Rust 中的整数类型</span>
|
||||
<span class="caption">表 3-1: Rust 中的整型</span>
|
||||
|
||||
| 长度 | 有符号类型 | 无符号类型 |
|
||||
|------------|---------|---------|
|
||||
@ -32,17 +32,17 @@ let guess: u32 = "42".parse().expect("Not a number!");
|
||||
| 16 位 | `i16` | `u16` |
|
||||
| 32 位 | `i32` | `u32` |
|
||||
| 64 位 | `i64` | `u64` |
|
||||
| 128-位 | `i128` | `u128` |
|
||||
| 视架构而定 | `isize` | `usize` |
|
||||
| 128 位 | `i128` | `u128` |
|
||||
| arch | `isize` | `usize` |
|
||||
|
||||
每个定义形式都可以是有符号类型或无符号类型,且带有一个显式的大小。**有符号**和**无符号**表示数字能否取负数或是只能取正数;也就是说,这个数是否可能是负数(有符号类型),或一直为正而不需要带上符号(无符号类型)。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)(译者补充:[“补码”百度百科](https://baike.baidu.com/item/%E8%A1%A5%E7%A0%81/6854613))形式存储。
|
||||
每个定义形式要么是有符号类型要么是无符号类型,且带有一个显式的大小。**有符号**和**无符号**表示数字能否取负数——也就是说,这个数是否可能是负数(有符号类型),或一直为正而不需要带上符号(无符号类型)。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)<!-- ignore -->(译者补充:[“补码”百度百科](https://baike.baidu.com/item/%E8%A1%A5%E7%A0%81/6854613))形式存储。
|
||||
|
||||
每个有符号类型规定的数字范围是 -(2<sup>n - 1</sup>) ~ 2<sup>n -
|
||||
1</sup> - 1,其中 `n` 是该定义形式的位长度。所以 `i8` 可存储数字范围是 -(2<sup>7</sup>) ~ 2<sup>7</sup> - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2<sup>n</sup> - 1,所以 `u8` 能够存储的数字为 0 ~ 2<sup>8</sup> - 1,即 0 ~ 255。
|
||||
|
||||
此外,`isize` 和 `usize` 类型取决于程序运行的计算机类型:64 位(如果使用 64 位架构系统)和 32 位(如果使用 32 位架构系统)。
|
||||
此外,`isize` 和 `usize` 类型取决于程序运行的计算机体系结构,在表中表示为“arch”:若使用 64 位架构系统则为 64 位,若使用 32 位架构系统则为 32 位。
|
||||
|
||||
整型的字面量可以写成下表 3-2 中任意一种。注意,除了字节字面量之外的所有的数字字面量都允许使用类型后缀,例如 `57u8`,还有可以使用 `_` 作为可视分隔符以方便读数,如 `1_000`,此值和 `1000` 相同。
|
||||
你可按表 3-2 中所示的任意形式来编写整型的字面量。注意,可能属于多种数字类型的数字字面量允许使用类型后缀来指定类型,例如 `57u8`。数字字面量还可以使用 `_` 作为可视分隔符以方便读数,如 `1_000`,此值和 `1000` 相同。
|
||||
|
||||
<span class="caption">表 3-2: Rust 的整型字面量</span>
|
||||
|
||||
@ -54,15 +54,15 @@ let guess: u32 = "42".parse().expect("Not a number!");
|
||||
| 二进制 | `0b1111_0000` |
|
||||
| 字节 (仅限于 `u8`) | `b'A'` |
|
||||
|
||||
那么该使用哪种类型的整型呢?如果不确定,Rust 的默认值通常是个不错的选择,整型默认是 `i32`:这通常是最快的,即便在 64 位系统上也是。`isize` 和 `usize` 的主要应用场景是用作某些集合的索引。
|
||||
那么该使用哪种类型的整型呢?如果不确定,Rust 的默认形式通常是个不错的选择,整型默认是 `i32`。`isize` 和 `usize` 的主要应用场景是用作某些集合的索引。
|
||||
|
||||
> ##### 整型溢出
|
||||
>
|
||||
> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**(*integer overflow*)。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 *panic*。Rust 使用这个术语来表明程序因错误而退出。第 9 章 [“`panic!` 与不可恢复的错误”][unrecoverable-errors-with-panic]<!-- ignore -->会详细介绍 panic。
|
||||
> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**(*integer overflow*),这会导致两种行为的其中一种。当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 *panic*。Rust 使用 panic 这个术语来表明程序因错误而退出。第 9 章 [“`panic!` 与不可恢复的错误”][unrecoverable-errors-with-panic]<!-- ignore -->会详细介绍 panic。
|
||||
>
|
||||
> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(*two’s complement wrapping*)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。
|
||||
> 在当使用 `--release` 参数进行发布(release)模式构建时,Rust **不**检测会导致 panic 的整型溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(*two’s complement wrapping*)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。
|
||||
>
|
||||
> 要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下的一系列方法:
|
||||
> 要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下一系列方法:
|
||||
>
|
||||
> - 使用 `wrapping_*` 方法在所有模式下进行包裹,例如 `wrapping_add`
|
||||
> - 如果使用 `checked_*` 方法时发生溢出,则返回 `None` 值
|
||||
@ -71,7 +71,7 @@ let guess: u32 = "42".parse().expect("Not a number!");
|
||||
|
||||
#### 浮点类型
|
||||
|
||||
**浮点类型数字** 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: `f32` 和 `f64`,分别为 32 位和 64 位大小。默认浮点类型是 `f64`,因为在现代的 CPU 中它的速度与 `f32` 几乎相同,但精度更高。
|
||||
**浮点数**(*floating-point number*)是带有小数点的数字,在 Rust 中浮点类型(简称浮点型)数字也有两种基本类型。Rust 的浮点型是 `f32` 和 `f64`,它们的大小分别为 32 位和 64 位。默认浮点类型是 `f64`,因为在现代的 CPU 中它的速度与 `f32` 的几乎相同,但精度更高。所有浮点型都是有符号的。
|
||||
|
||||
下面是一个演示浮点数的示例:
|
||||
|
||||
@ -81,11 +81,11 @@ let guess: u32 = "42".parse().expect("Not a number!");
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-06-floating-point/src/main.rs}}
|
||||
```
|
||||
|
||||
浮点数根据 IEEE-754 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度。
|
||||
浮点数按照 IEEE-754 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度浮点型。
|
||||
|
||||
#### 数字运算
|
||||
|
||||
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。整数除法会向下取整。下面代码演示了各使用一条 `let` 语句来说明相应运算的用法:
|
||||
Rust 的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取模运算。整数除法会向下取整。下面代码演示了各使用一条 `let` 语句来说明相应数字运算的用法:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -93,11 +93,11 @@ Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-07-numeric-operations/src/main.rs}}
|
||||
```
|
||||
|
||||
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录 B][appendix_b]<!-- ignore --> 罗列了 Rust 提供的所有运算符的列表。
|
||||
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录 B][appendix_b]<!-- ignore --> 罗列了 Rust 提供的所有运算符。
|
||||
|
||||
#### 布尔类型
|
||||
|
||||
和大多数编程语言一样,Rust 中的布尔类型也有两个可能的值:`true` 和 `false`。布尔值的大小为 1 个字节。Rust 中的布尔类型使用 `bool` 指定。例如:
|
||||
和大多数编程语言一样,Rust 中的布尔类型也有两个可能的值:`true` 和 `false`。布尔值的大小为 1 个字节。Rust 中的布尔类型使用 `bool` 声明。例如:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -105,11 +105,11 @@ Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-08-boolean/src/main.rs}}
|
||||
```
|
||||
|
||||
用到布尔值的地方主要是条件语句,如 `if` 语句。我们将会在[“控制流”][control-flow]<!-- ignore -->章节中介绍 `if` 在 Rust 中的用法。
|
||||
使用布尔值的主要地方是条件判断,如 `if` 表达式。我们将会在[“控制流”][control-flow]<!-- ignore -->章节中介绍 `if` 表达式在 Rust 中的用法。
|
||||
|
||||
#### 字符类型
|
||||
|
||||
到目前为止,我们只在用数字,不过 Rust 同样也支持字母。Rust 的 `char` (字符)类型是该语言最原始的字母类型,下面代码展示了使用它的一种方式(注意,`char` 字面量是用单引号括起来,而字符串字面量是用双引号扩起来):
|
||||
Rust 的 `char` (字符)类型是该语言最基本的字母类型,下面是一些声明 `char` 值的例子:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -117,17 +117,17 @@ Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-09-char/src/main.rs}}
|
||||
```
|
||||
|
||||
Rust 的字符类型表示的是一个 Unicode 值,这意味着它可以表示的不仅仅是 ASCII。标音字母,中文/日文/韩文的表意文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。Unicode 值的范围从 `U+0000`~`U+D7FF` 和 `U+E000`~`U+10FFFF`。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。我们将在第 8 章[“使用字符串存储 UTF-8 编码的文本”][strings]<!-- ignore -->中详细讨论这个主题。
|
||||
注意,我们声明的 `char` 字面量采用单引号括起来,这和字符串字面不同,字符串字面量是用双引号扩起来。Rust 的字符类型大小为 4 个字节,表示的是一个 Unicode 标量值,这意味着它可以表示的远远不止是 ASCII。标音字母,中文/日文/韩文的文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。Unicode 值的范围为 `U+0000` ~ `U+D7FF` 和 `U+E000`~`U+10FFFF`。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。我们将在第 8 章[“使用字符串存储 UTF-8 编码的文本”][strings]<!-- ignore -->中详细讨论这个主题。
|
||||
|
||||
### 复合类型
|
||||
|
||||
**复合类型**(*compound type*)可以将其他类型的多个值合在一块组成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。
|
||||
**复合类型**(*compound type*)可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。
|
||||
|
||||
#### 元组类型
|
||||
|
||||
元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小。
|
||||
|
||||
我们通过在括号内写入以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不要求是相同的。我们在下面示例中添加了可选的类型标注:
|
||||
我们通过在小括号内写入以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不要求是相同的。我们在下面示例中添加了可选的类型标注:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -143,7 +143,7 @@ Rust 的字符类型表示的是一个 Unicode 值,这意味着它可以表示
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-11-destructuring-tuples/src/main.rs}}
|
||||
```
|
||||
|
||||
该程序首先创建一个元组并将其绑定到变量 `tup` 上。 然后它借助 `let` 来使用一个模式匹配 `tup`,并将它分解成三个单独的变量 `x`、`y` 和 `z`。 这过程称为**解构**(*destructuring*),因为它将单个元组分为三部分。 最后,程序打印出 `y` 值,为 `6.4`。
|
||||
该程序首先创建一个元组并将其绑定到变量 `tup` 上。 然后它借助 `let` 来使用一个模式匹配 `tup`,并将它分解成三个单独的变量 `x`、`y` 和 `z`。 这过程称为**解构**(*destructuring*),因为它将单个元组分为三部分。最后,程序打印出 `y` 值,为 `6.4`。
|
||||
|
||||
除了通过模式匹配进行解构外,我们还可以使用一个句点(`.`)连上要访问的值的索引来直接访问元组元素。例如:
|
||||
|
||||
@ -155,13 +155,13 @@ Rust 的字符类型表示的是一个 Unicode 值,这意味着它可以表示
|
||||
|
||||
该程序创建一个元组 `x`,然后通过使用它们的索引为每个元素创建新的变量。和大多数编程语言一样,元组中的第一个索引为 0。
|
||||
|
||||
没有任何值的元组 `()` 是一种特殊的类型,只有一个值,也写成 `()`。该类型被称为**单元类型**(*unit type*),该值被称为**单元值**(*unit value*)。如果表达式不返回任何其他值,就隐式地返回单位值。
|
||||
没有任何值的元组 `()` 是一种特殊的类型,只有一个值,也写成 `()`。该类型被称为**单元类型**(*unit type*),该值被称为**单元值**(*unit value*)。如果表达式不返回任何其他值,就隐式地返回单元值。
|
||||
|
||||
#### 数组类型
|
||||
|
||||
将多个值的组合在一起另一种方式就是使用**数组**(*array*)。与元组不同,数组的每个元素必须具有相同的类型。与一些其他语言中的数组不同,Rust 中的数组具有固定长度。
|
||||
将多个值组合在一起的另一种方式就是使用**数组**(*array*)。与元组不同,数组的每个元素必须具有相同的类型。与某些其他语言中的数组不同,Rust 中的数组具有固定长度。
|
||||
|
||||
在 Rust 中,数组内的值将以逗号分隔的列表形式写入方括号内的:
|
||||
我们在方括号内以逗号分隔的列表形式将值写到数组中:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -169,25 +169,27 @@ Rust 的字符类型表示的是一个 Unicode 值,这意味着它可以表示
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-13-arrays/src/main.rs}}
|
||||
```
|
||||
|
||||
当你希望将数据分配到栈(stack)而不是堆(heap)时(我们将在[第 4 章][stack-and-heap]<!-- ignore -->)中进一步讨论栈和堆),或者当你希望确保始终具有固定数量的元素时,数组特别有用。但它们不像 vector (译注:中文字面翻译为“向量”,在 Rust 中意义为“动态数组,可变数组”)类型那么灵活。vector 类型类似于标准库中提供的集合类型,其的大小允许增长或缩小。如果不确定是使用数组还是 vector,那就应该使用一个 vector。[第 8 章][vectors]<!-- ignore -->将详细地讨论 vector。
|
||||
当你希望将数据分配到栈(stack)而不是堆(heap)时(我们将在[第 4 章][stack-and-heap]<!-- ignore -->)中进一步讨论栈和堆),或者当你希望确保始终具有固定数量的元素时,数组特别有用。但它们不像 vector (译注:中文字面翻译为“向量”,在 Rust 中意义为“动态数组,可变数组”)类型那么灵活。vector 类型类似于标准库中提供的集合类型,其大小**允许**增长或缩小。如果不确定是使用数组还是 vector,那就应该使用一个 vector。[第 8 章][vectors]<!-- ignore -->将详细地讨论 vector。
|
||||
|
||||
举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是 vector。这样的程序不太可能需要添加或删除月份,所以可以使用数组,因为你知道它总是包含 12 个元素:
|
||||
不过当你明确元素数量不需要改变时,数组会更有用。例如,如果你在程序中使用月份的名称,你很可能希望使用的是数组而不是 vector,因为你知道它始终包含 12 个元素:
|
||||
|
||||
```rust
|
||||
let months = ["January", "February", "March", "April", "May", "June", "July",
|
||||
"August", "September", "October", "November", "December"];
|
||||
```
|
||||
|
||||
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
|
||||
使用方括号编写数组的类型,其中包含每个元素的类型、分号,然后是数组中的元素数,如下所示:
|
||||
|
||||
```rust
|
||||
let a: [i32; 5] = [1, 2, 3, 4, 5];
|
||||
```
|
||||
|
||||
这里,`i32` 是每个元素的类型。分号之后,数字 `5` 表明该数组包含五个元素。
|
||||
这里,`i32` 是每个元素的类型。分号之后,数字 `5` 表明该数组包含 5 个元素。
|
||||
|
||||
以这种方式编写数组的类型看起来类似于初始化数组的另一种语法:如果要为每个元素创建包含相同值的数组,可以指定初始值,后跟分号,然后在方括号中指定数组的长度,如下所示:
|
||||
|
||||
对于每个元素都相同的情况,还可以通过指定初始值、后跟分号和方括号中的数组长度来初始化数组,如下所示:
|
||||
|
||||
```rust
|
||||
let a = [3; 5];
|
||||
```
|
||||
@ -231,7 +233,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
|
||||
该程序在索引操作中使用无效值时导致**运行时**(*runtime*)错误。程序退出并显示错误消息,未执行后面的 `println!` 语句。当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust会出现 `panic`。这种检查必须在运行时进行,尤其是在这种情况下,因为编译器可能无法知道用户之后运行代码时将输入什么值。
|
||||
|
||||
这是 Rust 在实践中安全原则的第一个例子。在很多低级语言中,并不进行这种检查,而且在你使用不正确的索引时,可以访问无效的内存。Rust 通过立即退出来的方式防止这种错误,而不是允许内存访问并继续运行程序。 第 9 章将进一步讨论 Rust 的错误处理。
|
||||
这是 Rust 在实践中安全原则的第一个例子。在很多低级语言中,并不进行这种检查,而且在你使用不正确的索引时,可以访问无效的内存。Rust 通过立即退出来的方式防止这种错误,而不是允许内存访问并继续运行程序。第 9 章将进一步讨论 Rust 的错误处理。
|
||||
|
||||
[comparing-the-guess-to-the-secret-number]:
|
||||
ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
函数在 Rust 代码中很普遍。你已经见过语言中最重要的函数之一:`main` 函数,它是很多程序的入口点。你也见过 `fn` 关键字,它用来声明新函数。
|
||||
|
||||
Rust 代码中的函数和变量名使用 *snake case* 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:
|
||||
Rust 代码中的函数和变量名使用下划线命名法(*snake case*,直译为蛇形命名法)规范风格。在下划线命名法中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -10,11 +10,11 @@ Rust 代码中的函数和变量名使用 *snake case* 规范风格。在 snake
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-16-functions/src/main.rs}}
|
||||
```
|
||||
|
||||
Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
|
||||
Rust 中的函数定义以 `fn` 开始,后跟着函数名和一对圆括号。大括号告诉编译器函数体在哪里开始和结束。
|
||||
|
||||
可以使用函数名后跟圆括号来调用我们定义过的任意函数。因为程序中已定义 `another_function` 函数,所以可以在 `main` 函数中调用它。注意,源码中 `another_function` 定义在 `main` 函数**之后**;也可以定义在之前。Rust 不关心函数定义于何处,只要定义了就行。
|
||||
|
||||
让我们新建一个叫做 *functions* 的二进制项目来进一步探索函数。将上面的 `another_function` 例子写入 *src/main.rs* 中并运行。你应该会看到如下输出:
|
||||
让我们新建一个叫做 *functions* 的二进制项目来进一步探讨函数。将上面的 `another_function` 例子写入 *src/main.rs* 中并运行。你应该会看到如下输出:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-16-functions/output.txt}}
|
||||
@ -22,11 +22,11 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
`main` 函数中的代码会按顺序执行。首先,打印 “Hello, world!” 信息,然后调用 `another_function` 函数并打印它的信息。
|
||||
|
||||
### 函数参数
|
||||
### 参数
|
||||
|
||||
函数也可以被定义为拥有**参数**(*parameters*),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(*arguments*),但是在日常交流中,人们倾向于不区分使用 *parameter* 和 *argument* 来表示函数定义中的变量或调用函数时传入的具体值。
|
||||
函数也可以被定义为拥有**参数**(*parameter*),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为**实参**(*argument*),但是在日常交流中,人们倾向于不区分使用 *parameter* 和 *argument* 来表示函数定义中的变量或调用函数时传入的具体值。
|
||||
|
||||
下面被重写的 `another_function` 版本展示了 Rust 中参数是什么样的:
|
||||
在这个版本的 `another_function` 中,我们添加了一个参数:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -42,7 +42,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
`another_function` 的声明中有一个命名为 `x` 的参数。`x` 的类型被指定为 `i32`。当将 `5` 传给 `another_function` 时,`println!` 宏将 `5` 放入格式化字符串中大括号的位置。
|
||||
|
||||
在函数签名中,**必须**声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。
|
||||
在函数签名中,**必须**声明每个参数的类型。这是一个 Rust 设计中经过慎重考虑的决定:要求在函数定义中提供类型标注,意味着编译器几乎从不需要你在代码的其他地方注明类型来指出你的意图。
|
||||
|
||||
当一个函数有多个参数时,使用逗号分隔,像这样:
|
||||
|
||||
@ -54,7 +54,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
这个例子创建了一个有两个参数的名为 `print_labeled_measurement` 的函数。第一个参数名为 `value`, 类型是 `i32`。第二个参数是 `unit_label` ,类型是 `char`。接着该函数打印包含 `value` 和 `unit_label` 的文本。
|
||||
|
||||
尝试运行代码。使用上面的例子替换当前 *functions* 项目的 *src/main.rs* 文件,并用 `cargo run` 运行它:
|
||||
让我们尝试运行这段代码。使用上面的例子替换当前 *functions* 项目的 *src/main.rs* 文件,并用 `cargo run` 运行它:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-18-functions-with-multiple-parameters/output.txt}}
|
||||
@ -64,9 +64,9 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
### 语句和表达式
|
||||
|
||||
函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们只介绍了没有结尾表达式的函数,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的独特地方。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。
|
||||
函数体由一系列语句组成,也可选地以表达式结尾。目前为止,我们介绍的函数还没有包含结尾表达式,不过你已经看到了表达式作为语句的一部分。因为 Rust 是一门基于表达式(expression-based)的语言,所以这是一个需要理解的重要区别。其他语言没有这样的区别,所以让我们看看语句和表达式分别是什么,以及它们的区别如何影响函数体。
|
||||
|
||||
**语句**(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看一些例子:
|
||||
**语句**(*statement*)是执行一些操作但不返回值的指令。表达式(*expression*)计算并产生一个值。让我们看一些例子:
|
||||
|
||||
实际上,我们已经使用过语句和表达式。使用 `let` 关键字创建变量并绑定一个值是一个语句。在列表 3-1 中,`let y = 6;` 是一个语句。
|
||||
|
||||
@ -80,7 +80,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
函数定义也是语句,上面整个例子本身就是一个语句。
|
||||
|
||||
语句不返回值。因此,不能把 `let` 语句赋值给另一个变量,比如下面的例子尝试做的,会产生一个错误:
|
||||
语句不返回值。因此,不能把 `let` 语句赋值给另一个变量,就像下面的代码尝试做的那样,会产生一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -96,7 +96,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
|
||||
`let y = 6` 语句并不返回值,所以没有可以绑定到 `x` 上的值。这与其他语言不同,例如 C 和 Ruby,它们的赋值语句会返回所赋的值。在这些语言中,可以这么写 `x = y = 6`,这样 `x` 和 `y` 的值都是 `6`;Rust 中不能这样写。
|
||||
|
||||
表达式会计算出一个值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个简单的数学运算,比如 `5 + 6`,这是一个表达式并计算出值 `11`。表达式可以是语句的一部分:在示例 3-1 中,语句 `let y = 6;` 中的 `6` 是一个表达式,它计算出的值是 `6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块),`{}`,也是一个表达式,例如:
|
||||
表达式会计算出一个值,并且你接下来要用 Rust 编写的大部分代码都由表达式组成。考虑一个数学运算,比如 `5 + 6`,这是一个表达式并计算出值 `11`。表达式可以是语句的一部分:在示例 3-1 中,语句 `let y = 6;` 中的 `6` 是一个表达式,它计算出的值是 `6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块) `{}` 也是一个表达式,例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -113,7 +113,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
}
|
||||
```
|
||||
|
||||
是一个代码块,它的值是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意结尾没有分号的那一行 `x+1`,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。
|
||||
是一个代码块,在这个例子中计算结果是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意,`x+1` 行的末尾没有分号,这与你目前见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的末尾加上分号,那么它就转换为语句,而语句不会返回值。在接下来探讨函数返回值和表达式时,请记住这一点。
|
||||
|
||||
### 带有返回值的函数
|
||||
|
||||
@ -125,7 +125,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-21-function-return-values/src/main.rs}}
|
||||
```
|
||||
|
||||
在 `five` 函数中没有函数调用、宏、甚至没有 `let` 语句——只有数字 `5`。这在 Rust 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是 `-> i32`。尝试运行代码;输出应该看起来像这样:
|
||||
在 `five` 函数中没有函数调用、宏,甚至没有 `let` 语句——只有数字 `5` 本身。这在 Rust 中是一个完全有效的函数。注意,函数返回值的类型也被指定好,即 `-> i32`。尝试运行代码;输出应如下所示:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-21-function-return-values/output.txt}}
|
||||
@ -137,7 +137,7 @@ Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大
|
||||
let x = 5;
|
||||
```
|
||||
|
||||
其次,`five` 函数没有参数并定义了返回值类型,不过函数体只有单单一个 `5` 也没有分号,因为这是一个表达式,我们想要返回它的值。
|
||||
其次,`five` 函数没有参数并定义了返回值类型,不过函数体只有单单一个 `5` 也没有分号,因为这是一个表达式,正是我们想要返回的值。
|
||||
|
||||
让我们看看另一个例子:
|
||||
|
||||
@ -147,7 +147,7 @@ let x = 5;
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-22-function-parameter-and-return/src/main.rs}}
|
||||
```
|
||||
|
||||
运行代码会打印出 `The value of x is: 6`。但如果在包含 `x + 1` 的行尾加上一个分号,把它从表达式变成语句,我们将看到一个错误。
|
||||
运行代码会打印出 `The value of x is: 6`。但如果在包含 `x + 1` 的行尾加上一个分号,把它从表达式变成语句,我们将得到一个错误。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -161,4 +161,4 @@ let x = 5;
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-23-statements-dont-return-values/output.txt}}
|
||||
```
|
||||
|
||||
主要的错误信息,“mismatched types”(类型不匹配),揭示了代码的核心问题。函数 `plus_one` 的定义说明它要返回一个 `i32` 类型的值,不过语句并不会返回值,使用单位类型 `()` 表示不返回值。因为不返回值与函数定义相矛盾,从而出现一个错误。在输出中,Rust 提供了一条信息,可能有助于纠正这个错误:它建议删除分号,这会修复这个错误。
|
||||
主要的错误信息 “mismatched types”(类型不匹配)揭示了这段代码的核心问题。函数 `plus_one` 的定义说明它要返回一个 `i32` 类型的值,不过语句并不会返回值,此值由单位类型 `()` 表示,表示不返回值。因为不返回值与函数定义相矛盾,从而出现一个错误。在输出中,Rust 提供了一条信息,可能有助于纠正这个错误:它建议删除分号,这将修复错误。
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 注释
|
||||
|
||||
很多开发者都在努力使他们的代码容易理解,但有时需要额外的解释。在这种情况下,开发者在它们的源代码中留下笔记或注释,编译器将会忽略掉这些内容,但读源代码的人可能会发现有用。
|
||||
所有的开发者都在努力使他们的代码容易理解,但有时需要额外的解释。在这种情况下,开发者在他们的源码中留下**注释**,编译器将会忽略掉这些内容,但阅读源码的人可能会发现有用。
|
||||
|
||||
这是一条简单的注释:
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
// Hello, world.
|
||||
```
|
||||
|
||||
在 Rust 中,惯用的注释形式以两个斜杆开头,直到该行尾结束。对于超出单行的注释,需要在注释的每行行首加上 `//`,如下所示:
|
||||
在 Rust 中,惯用的注释形式以两个斜杆开头,直到该行尾结束。对于超出单行的注释,需要在每行的行首加上 `//`,如下所示:
|
||||
|
||||
```rust
|
||||
// 我们在这里处理一些复杂事情,需要足够长的解释,使用
|
||||
@ -24,7 +24,7 @@
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-24-comments-end-of-line/src/main.rs}}
|
||||
```
|
||||
|
||||
不过下面的这种格式会更常见,将注释放到需要解释的代码的上面单独一行:
|
||||
不过下面的这种格式会更常见,将注释放到需要解释的代码上面的单独行:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
## 控制流
|
||||
|
||||
根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 `if` 表达式和循环。
|
||||
根据条件是否为真来决定是否执行某些代码,或根据条件是否为真来重复运行一段代码,是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 `if` 表达式和循环。
|
||||
|
||||
### `if` 表达式
|
||||
|
||||
`if` 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
|
||||
|
||||
在 *projects* 目录新建一个叫做 *branches* 的项目,来学习 `if` 表达式。在 *src/main.rs* 文件中,输入如下内容:
|
||||
在 *projects* 目录新建一个名为 *branches* 的项目用来学习 `if` 表达式。在 *src/main.rs* 文件中,输入如下内容:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-26-if-true/src/main.rs}}
|
||||
```
|
||||
|
||||
所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 的值是否小于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做 *arms*,就像第 2 章[“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number]<!-- ignore -->部分中讨论到的 `match` 表达式中的分支一样。
|
||||
所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 的值是否小于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做**分支**(*arm*),就像第 2 章[“比较猜测的数字和秘密数字”][comparing-the-guess-to-the-secret-number]<!-- ignore -->部分中讨论到的 `match` 表达式中的分支一样。
|
||||
|
||||
也可以包含一个可选的 `else` 表达式来提供一个在条件为假时应当执行的代码块,这里我们就这么做了。如果不提供 `else` 表达式并且条件为假时,程序会直接忽略 `if` 代码块并继续执行下面的代码。
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-28-if-condition-must-be-bool/output.txt}}
|
||||
```
|
||||
|
||||
这个错误表明 Rust 期望一个 `bool` 却得到了一个整数。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 `if` 的条件。例如,如果想要 `if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改成下面这样:
|
||||
这个错误表明 Rust 期望一个 `bool` 却得到了一个整数。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。你必须自始至终显式地使用布尔值作为 `if` 的条件。例如,如果想要 `if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改成下面这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
#### 使用 `else if` 处理多重条件
|
||||
|
||||
可以将 `else if` 表达式与 `if` 和 `else` 组合来实现多重条件。例如:
|
||||
可以将 `if` 和 `else` 组成的 `else if` 表达式来实现多重条件。例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-30-else-if/src/main.rs}}
|
||||
```
|
||||
|
||||
这个程序有四个可能的执行路径。运行后应该能看到如下输出:
|
||||
这个程序有 4 个可能的执行路径。运行后应该能看到如下输出:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/no-listing-30-else-if/output.txt}}
|
||||
@ -78,11 +78,11 @@
|
||||
|
||||
当执行这个程序时,它按顺序检查每个 `if` 表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会输出 `number is divisible by 2`,更不会输出 `else` 块中的 `number is not divisible by 4, 3, or 2`。原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。
|
||||
|
||||
使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if` 表达式,最好重构代码。为此,第 6 章会介绍一个强大的 Rust 分支结构(branching construct),叫做 `match`。
|
||||
使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if` 表达式,最好重构代码。为处理这些情况,第 6 章会介绍一个强大的 Rust 分支结构(branching construct),叫做 `match`。
|
||||
|
||||
#### 在 `let` 语句中使用 `if`
|
||||
|
||||
因为 `if` 是一个表达式,我们可以在 `let` 语句的右侧使用它,例如在示例 3-2 中:
|
||||
因为 `if` 是一个表达式,我们可以在 `let` 语句的右侧使用它来将结果赋值给一个变量,例如在示例 3-2 中:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
{{#include ../listings/ch03-common-programming-concepts/listing-03-02/output.txt}}
|
||||
```
|
||||
|
||||
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 `if` 表达式的值取决于哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在示例 3-2 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。如果它们的类型不匹配,如下面这个例子,则会出现一个错误:
|
||||
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 `if` 表达式的值取决于哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在示例 3-2 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。如果它们的类型不匹配,如下面这个例子,则会产生一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
|
||||
### 使用循环重复执行
|
||||
|
||||
多次执行同一段代码是很常用的,Rust 为此提供了多种 **循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们新建一个叫做 *loops* 的项目。
|
||||
多次执行同一段代码是很常用的,Rust 为此提供了多种**循环**(*loop*),它们遍历执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了试验循环,让我们新建一个名为 *loops* 的项目。
|
||||
|
||||
Rust 有三种循环:`loop`、`while` 和 `for`。我们每一个都试试。
|
||||
|
||||
@ -124,7 +124,7 @@ Rust 有三种循环:`loop`、`while` 和 `for`。我们每一个都试试。
|
||||
|
||||
`loop` 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
|
||||
|
||||
作为一个例子,将 *loops* 目录中的 *src/main.rs* 文件修改为如下:
|
||||
例如,将 *loops* 目录中的 *src/main.rs* 文件修改为如下:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -132,7 +132,7 @@ Rust 有三种循环:`loop`、`while` 和 `for`。我们每一个都试试。
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-loop/src/main.rs}}
|
||||
```
|
||||
|
||||
当运行这个程序时,我们会看到连续的反复打印 `again!`,直到我们手动停止程序。大部分终端都支持一个快捷键,<span class="keystroke">ctrl-c</span>,来终止一个陷入无限循环的程序。尝试一下:
|
||||
当运行这个程序时,我们会看到连续的反复打印 `again!`,直到我们手动停止程序。大部分终端都支持一个快捷键 <span class="keystroke">ctrl-c</span> 来终止一个陷入无限循环的程序。尝试一下:
|
||||
|
||||
<!-- manual-regeneration
|
||||
cd listings/ch03-common-programming-concepts/no-listing-32-loop
|
||||
@ -154,7 +154,7 @@ again!
|
||||
|
||||
符号 `^C` 代表你在这按下了<span class="keystroke">ctrl-c</span>。在 `^C` 之后你可能看到也可能看不到 `again!` ,这取决于在接收到终止信号时代码执行到了循环的何处。
|
||||
|
||||
幸运的是,Rust 也提供了一种从代码中跳出循环的方法。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第 2 章猜数字游戏的 [“猜测正确后退出”][quitting-after-a-correct-guess]<!-- ignore --> 部分使用过它来在用户猜对数字赢得游戏后退出程序。
|
||||
幸运的是,Rust 也提供了一种从代码中跳出循环的方法。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第 2 章猜数字游戏的 [“猜测正确后退出”][quitting-after-a-correct-guess]<!-- ignore --> 章节使用过它来在用户猜对数字赢得游戏后退出程序。
|
||||
|
||||
我们在猜数字游戏中也使用了 `continue`。循环中的 `continue` 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。
|
||||
|
||||
@ -172,7 +172,7 @@ again!
|
||||
|
||||
#### 从循环返回
|
||||
|
||||
`loop` 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你可能会需要将操作的结果传递给其它的代码。如果将返回值加入你用来停止循环的 `break` 表达式,它会被停止的循环返回:
|
||||
`loop` 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你可能会需要将操作的结果从循环中传递给其它的代码。为此,你可以在用于停止循环的 `break` 表达式添加你想要返回的值;该值将从循环中返回,以便您可以使用它,如下所示:
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-33-return-value-from-loop/src/main.rs}}
|
||||
@ -182,9 +182,8 @@ again!
|
||||
|
||||
#### `while` 条件循环
|
||||
|
||||
在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 `break` 停止循环。这个循环类型可以通过组合 `loop`、`if`、`else` 和 `break` 来实现;如果你喜欢的话,现在就可以在程序中试试。
|
||||
在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 `break` 停止循环。这个循环类型可以通过组合 `loop`、`if`、`else` 和 `break` 来实现;如果你喜欢的话,现在就可以在程序中试试。然而,这个模式太常用了,Rust 为此内置了一个语言结构,它被称为 `while` 循环。示例 3-3 使用了 `while` 来程序循环 3 次,每次数字都减 1。接着在循环结束后,打印出另一个信息并退出。
|
||||
|
||||
然而,这个模式太常用了,Rust 为此内置了一个语言结构,它被称为 `while` 循环。示例 3-3 使用了 `while`:程序循环三次,每次数字都减一。接着,在循环结束后,打印出另一个信息并退出。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -198,7 +197,7 @@ again!
|
||||
|
||||
#### 使用 `for` 遍历集合
|
||||
|
||||
可以使用 `while` 结构来遍历集合中的元素,比如数组。例如,看看示例 3-4。
|
||||
可以使用 `while` 结构来遍历集合中的元素,比如数组。例如,示例 3-4 中的循环打印数组 `a` 中的每个元素。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -208,15 +207,13 @@ again!
|
||||
|
||||
<span class="caption">示例 3-4:使用 `while` 循环遍历集合中的元素</span>
|
||||
|
||||
在这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素:
|
||||
在这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(即 `index < 5` 不再为真时)。运行这段代码会打印出数组中的每一个元素:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch03-common-programming-concepts/listing-03-04/output.txt}}
|
||||
```
|
||||
|
||||
数组中的所有五个元素都如期被打印出来。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
|
||||
|
||||
但这个过程很容易出错;如果索引长度或测试条件不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环进行条件检查,以确定在循环的每次迭代中索引是否在数组的边界内。
|
||||
数组中的 5 个元素全部都如期被打印出来。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第 6 个值(会越界)之前就停止了。
|
||||
|
||||
但是这个过程很容易出错;如果索引值或测试条件不正确会导致程序 panic。例如,如果将 `a` 数组的定义更改为包含 4 个元素,但忘记将条件更新为`while index < 4`,则代码会出现 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环进行条件检查,以确定在循环的每次迭代中索引是否在数组的边界内。
|
||||
|
||||
@ -232,11 +229,11 @@ again!
|
||||
|
||||
当运行这段代码时,将看到与示例 3-4 一样的输出。更为重要的是,我们增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。
|
||||
|
||||
使用 `for` 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了,就像使用清单 3-4 中使用的方法一样。
|
||||
使用 `for` 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了,就像使用示例 3-4 中使用的方法一样。
|
||||
|
||||
`for` 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。
|
||||
|
||||
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range:
|
||||
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转区间(range):
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -244,17 +241,17 @@ again!
|
||||
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-34-for-range/src/main.rs}}
|
||||
```
|
||||
|
||||
这段代码看起来更帅气不是吗?
|
||||
这段代码更好一些,不是吗?
|
||||
|
||||
## 总结
|
||||
|
||||
你做到了!这是一个大章节:你学习了变量、标量和复合数据类型、函数、注释、 `if` 表达式和循环!如果你想要实践本章讨论的概念,尝试构建如下程序:
|
||||
你做到了!这是一个大章节:你学习了变量、标量和复合数据类型、函数、注释、`if` 表达式和循环!要练习本章讨论的概念,请尝试编写程序来实现以下操作:
|
||||
|
||||
* 相互转换摄氏与华氏温度。
|
||||
* 生成 n 阶斐波那契数列。
|
||||
* 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,并利用歌曲中的重复部分(编写循环)。
|
||||
- 在华氏温度和摄氏度之间转换温度。
|
||||
- 生成 n 阶斐波那契数列。
|
||||
- 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,利用歌曲中的重复部分(编写循环)。
|
||||
|
||||
当你准备好继续的时候,让我们讨论一个其他语言中**并不**常见的概念:所有权(ownership)。
|
||||
当你准备好继续前进时,我们将讨论一个其他语言中**并不**常见的概念:所有权(ownership)。
|
||||
|
||||
[comparing-the-guess-to-the-secret-number]:
|
||||
ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字
|
||||
|
@ -196,7 +196,7 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
|
||||
|
||||
原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
|
||||
|
||||
Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第 10 章详细讲解 trait)。如果一个类型实现了 `Copy` trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。要学习如何为你的类型添加 `Copy` 注解以实现该 trait,请阅读附录 C 中的 [“可派生的 trait”][derivable-traits]<!-- ignore -->。
|
||||
Rust 有一个叫做 `Copy` trait 的特殊标注,可以用在类似整型这样的存储在栈上的类型上(第 10 章详细讲解 trait)。如果一个类型实现了 `Copy` trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 标注,将会出现一个编译时错误。要学习如何为你的类型添加 `Copy` 标注以实现该 trait,请阅读附录 C 中的 [“可派生的 trait”][derivable-traits]<!-- ignore -->。
|
||||
|
||||
那么哪些类型实现了 `Copy` trait 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 `Copy`,任何不需要分配内存或某种形式资源的类型都可以实现 `Copy` 。如下是一些 `Copy` 的类型:
|
||||
|
||||
|
@ -12,9 +12,9 @@ let v: Vec<i32> = Vec::new();
|
||||
|
||||
<span class="caption">示例 8-1:新建一个空的 vector 来储存 `i32` 类型的值</span>
|
||||
|
||||
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这一点非常重要。vector 是用泛型实现的,第 10 章会涉及到如何对你自己的类型使用它们。现在,我们知道 `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<i32>`:
|
||||
在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型标注。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。示例 8-2 新建一个拥有值 `1`、`2` 和 `3` 的 `Vec<i32>`:
|
||||
|
||||
```rust
|
||||
let v = vec![1, 2, 3];
|
||||
@ -22,7 +22,7 @@ let v = vec![1, 2, 3];
|
||||
|
||||
<span class="caption">示例 8-2:新建一个包含初值的 vector</span>
|
||||
|
||||
因为我们提供了 `i32` 类型的初始值,Rust 可以推断出 `v` 的类型是 `Vec<i32>`,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。
|
||||
因为我们提供了 `i32` 类型的初始值,Rust 可以推断出 `v` 的类型是 `Vec<i32>`,因此类型标注就不是必须的。接下来让我们看看如何修改一个 vector。
|
||||
|
||||
### 更新 vector
|
||||
|
||||
@ -39,7 +39,7 @@ v.push(8);
|
||||
|
||||
<span class="caption">示例 8-3:使用 `push` 方法向 vector 增加值</span>
|
||||
|
||||
如第 3 章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 `Vec<i32>` 注解。
|
||||
如第 3 章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 `Vec<i32>` 标注。
|
||||
|
||||
### 丢弃 vector 时也会丢弃其所有元素
|
||||
|
||||
|
@ -38,7 +38,7 @@ let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
|
||||
|
||||
<span class="caption">示例 8-21:用队伍列表和分数列表创建哈希 map</span>
|
||||
|
||||
这里 `HashMap<_, _>` 类型注解是必要的,因为 `collect` 有可能当成多种不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
|
||||
这里 `HashMap<_, _>` 类型标注是必要的,因为 `collect` 有可能当成多种不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
|
||||
|
||||
### 哈希 map 和所有权
|
||||
|
||||
|
@ -29,7 +29,7 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 9-3:打开文件</span>
|
||||
|
||||
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看 [标准库 API 文档](https://rustwiki.org/zh-CN/std/index.html)<!-- ignore -->,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下:
|
||||
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看 [标准库 API 文档](https://rustwiki.org/zh-CN/std/index.html)<!-- ignore -->,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型标注,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下:
|
||||
|
||||
```rust,ignore,does_not_compile
|
||||
let f: u32 = File::open("hello.txt");
|
||||
|
@ -59,7 +59,7 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作
|
||||
} // ---------+
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-18:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b`</span>
|
||||
<span class="caption">示例 10-18:`r` 和 `x` 的生命周期标注,分别叫做 `'a` 和 `'b`</span>
|
||||
|
||||
这里将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。
|
||||
|
||||
@ -137,11 +137,11 @@ signature does not say whether it is borrowed from `x` or `y`
|
||||
|
||||
当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-18 和 10-19 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
|
||||
|
||||
### 生命周期注解语法
|
||||
### 生命周期标注语法
|
||||
|
||||
生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。
|
||||
生命周期标注并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期标注描述了多个引用生命周期相互的关系,而不影响其生命周期。
|
||||
|
||||
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。
|
||||
生命周期标注有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。`'a` 是大多数人默认使用的名称。生命周期参数标注位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期标注分隔开。
|
||||
|
||||
这里有一些例子:我们有一个没有生命周期参数的 `i32` 的引用,一个有叫做 `'a` 的生命周期参数的 `i32` 的引用,和一个生命周期也是 `'a` 的 `i32` 的可变引用:
|
||||
|
||||
@ -151,9 +151,9 @@ signature does not say whether it is borrowed from `x` or `y`
|
||||
&'a mut i32 // 带有显式生命周期的可变引用
|
||||
```
|
||||
|
||||
单个生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。
|
||||
单个生命周期标注本身没有多少意义,因为生命周期标注告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期标注意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。
|
||||
|
||||
### 函数签名中的生命周期注解
|
||||
### 函数签名中的生命周期标注
|
||||
|
||||
现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-22 中在每个引用中都加上了 `'a` 那样:
|
||||
|
||||
@ -175,7 +175,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
|
||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这就是我们告诉 Rust 需要其保证的约束条件。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||
|
||||
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。
|
||||
当在函数中使用生命周期标注时,这些标注出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。
|
||||
|
||||
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。
|
||||
|
||||
@ -296,9 +296,9 @@ function body at 1:1...
|
||||
|
||||
综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
|
||||
|
||||
### 结构体定义中的生命周期注解
|
||||
### 结构体定义中的生命周期标注
|
||||
|
||||
目前为止,我们只定义过有所有权类型的结构体。接下来,我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`:
|
||||
目前为止,我们只定义过有所有权类型的结构体。接下来,我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期标注。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -316,15 +316,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-25:一个存放引用的结构体,所以其定义需要生命周期注解</span>
|
||||
<span class="caption">示例 10-25:一个存放引用的结构体,所以其定义需要生命周期标注</span>
|
||||
|
||||
这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。
|
||||
这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个标注意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。
|
||||
|
||||
这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。`novel` 的数据在 `ImportantExcerpt` 实例创建之前就存在。另外,直到 `ImportantExcerpt` 离开作用域之后 `novel` 都不会离开作用域,所以 `ImportantExcerpt` 实例中的引用是有效的。
|
||||
|
||||
### 生命周期省略(Lifetime Elision)
|
||||
|
||||
现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第 4 章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期注解却能编译成功:
|
||||
现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第 4 章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期标注却能编译成功:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -342,25 +342,25 @@ fn first_word(s: &str) -> &str {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-26:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用</span>
|
||||
<span class="caption">示例 10-26:示例 4-9 定义了一个没有使用生命周期标注的函数,即便其参数和返回值都是引用</span>
|
||||
|
||||
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
这个函数没有生命周期标注却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
|
||||
```rust,ignore
|
||||
fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
```
|
||||
|
||||
在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 开发者们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制开发者显式的增加注解。
|
||||
在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 开发者们总是重复地编写一模一样的生命周期标注。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制开发者显式的增加标注。
|
||||
|
||||
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的。未来只会需要更少的生命周期注解。
|
||||
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的。未来只会需要更少的生命周期标注。
|
||||
|
||||
被编码进 Rust 引用分析的模式被称为 **生命周期省略规则**(*lifetime elision rules*)。这并不是需要开发者遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
|
||||
|
||||
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。
|
||||
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期标注来解决。
|
||||
|
||||
函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。
|
||||
|
||||
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 `fn` 定义,以及 `impl` 块。
|
||||
编译器采用三条规则来判断引用何时不需要明确的标注。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 `fn` 定义,以及 `impl` 块。
|
||||
|
||||
第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
|
||||
@ -404,13 +404,13 @@ fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
|
||||
|
||||
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
|
||||
|
||||
### 方法定义中的生命周期注解
|
||||
### 方法定义中的生命周期标注
|
||||
|
||||
当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法。声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。
|
||||
|
||||
(实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
|
||||
|
||||
`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-25 中定义的结构体 `ImportantExcerpt` 的例子。
|
||||
`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期标注。让我们看看一些使用示例 10-25 中定义的结构体 `ImportantExcerpt` 的例子。
|
||||
|
||||
首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值:
|
||||
|
||||
@ -480,9 +480,9 @@ 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 bounds 以及泛型生命周期类型,你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期标注所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!
|
||||
|
||||
你可能不会相信,这个话题还有更多需要学习的内容:第 17 章会讨论 trait 对象,这是另一种使用 trait 的方式。第 19 章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!
|
||||
你可能不会相信,这个话题还有更多需要学习的内容:第 17 章会讨论 trait 对象,这是另一种使用 trait 的方式。第 19 章会涉及到生命周期标注更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!
|
||||
|
||||
[references-and-borrowing]:
|
||||
ch04-02-references-and-borrowing.html#references-and-borrowing
|
||||
|
@ -8,4 +8,4 @@ Edsger W. Dijkstra 在其 1972 年的文章【谦卑的开发者】(“The Hum
|
||||
|
||||
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,返回值是 `5`。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。
|
||||
|
||||
测试是一项复杂的技能:虽然不能在一个章节的篇幅中介绍如何编写好的测试的每个细节,但我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
||||
测试是一项复杂的技能:虽然不能在一个章节的篇幅中介绍如何编写好的测试的每个细节,但我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的标注和宏,运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
||||
|
@ -10,7 +10,7 @@ Rust 中的测试函数是用来验证非测试代码是否按照期望的方式
|
||||
|
||||
### 测试函数剖析
|
||||
|
||||
作为最简单例子,Rust 中的测试就是一个带有 `test` 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第 5 章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 `test` 属性的函数,并报告每一个测试是通过还是失败。
|
||||
作为最简单例子,Rust 中的测试就是一个带有 `test` 属性标注的函数。属性(attribute)是关于 Rust 代码片段的元数据;第 5 章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 `test` 属性的函数,并报告每一个测试是通过还是失败。
|
||||
|
||||
第 7 章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块!
|
||||
|
||||
@ -41,7 +41,7 @@ mod tests {
|
||||
|
||||
<span class="caption">示例 11-1:由 `cargo new` 自动生成的测试模块和函数</span>
|
||||
|
||||
现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解,并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。因为也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
|
||||
现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 标注,并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。因为也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
|
||||
|
||||
函数体通过使用 `assert_eq!` 宏来断言 2 加 2 等于 4。一个典型的测试的格式,就是像这个例子中的断言一样。接下来运行就可以看到测试通过。
|
||||
|
||||
@ -358,7 +358,7 @@ 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 章示例 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 的详细信息。
|
||||
|
||||
### 自定义失败信息
|
||||
|
||||
@ -627,7 +627,7 @@ mod tests {
|
||||
|
||||
这样编写测试来返回 `Result<T, E>` 就可以在函数体中使用问号运算符,如此可以方便的编写任何运算符会返回 `Err` 成员的测试。
|
||||
|
||||
不能对这些使用 `Result<T, E>` 的测试使用 `#[should_panic]` 注解。相反应该在测试失败时直接返回 `Err` 值。
|
||||
不能对这些使用 `Result<T, E>` 的测试使用 `#[should_panic]` 标注。相反应该在测试失败时直接返回 `Err` 值。
|
||||
|
||||
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
#### 测试模块和 `#[cfg(test)]`
|
||||
|
||||
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
||||
测试模块的 `#[cfg(test)]` 标注告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 标注。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
||||
|
||||
回忆本章第一部分新建的 `adder` 项目,Cargo 为我们生成了如下代码:
|
||||
|
||||
|
@ -217,17 +217,17 @@ fn generate_workout(intensity: u32, random_number: u32) {
|
||||
|
||||
现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行该代码。
|
||||
|
||||
然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这调用了慢计算代码两次而使得用户需要多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过闭包可以提供另外一种解决方案。我们稍后会讨论这个方案,不过目前让我们首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。
|
||||
然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这调用了慢计算代码两次而使得用户需要多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过闭包可以提供另外一种解决方案。我们稍后会讨论这个方案,不过目前让我们首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型标注。
|
||||
|
||||
### 闭包类型推断和注解
|
||||
### 闭包类型推断和标注
|
||||
|
||||
闭包不要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
|
||||
闭包不要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型标注是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
|
||||
|
||||
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。
|
||||
|
||||
强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。
|
||||
|
||||
类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解;为示例 13-5 中定义的闭包标注类型将看起来像示例 13-7 中的定义:
|
||||
类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型标注;为示例 13-5 中定义的闭包标注类型将看起来像示例 13-7 中的定义:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -242,9 +242,9 @@ let expensive_closure = |num: u32| -> u32 {
|
||||
};
|
||||
```
|
||||
|
||||
<span class="caption">示例 13-7:为闭包的参数和返回值增加可选的类型注解</span>
|
||||
<span class="caption">示例 13-7:为闭包的参数和返回值增加可选的类型标注</span>
|
||||
|
||||
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法之外:
|
||||
有了类型标注闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法之外:
|
||||
|
||||
```rust,ignore
|
||||
fn add_one_v1 (x: u32) -> u32 { x + 1 }
|
||||
@ -253,9 +253,9 @@ let add_one_v3 = |x| { x + 1 };
|
||||
let add_one_v4 = |x| x + 1 ;
|
||||
```
|
||||
|
||||
第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。这些都是有效的闭包定义,并在调用时产生相同的行为。
|
||||
第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型标注,而第四行去掉了可选的大括号,因为闭包体只有一行。这些都是有效的闭包定义,并在调用时产生相同的行为。
|
||||
|
||||
闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `u32`,则会得到一个错误:
|
||||
闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型标注:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `u32`,则会得到一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
|
@ -8,7 +8,7 @@ Rust 和 Cargo 有一些帮助别人更方便找到和使用你发布的包的
|
||||
|
||||
准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第 3 章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(_documentation comments_),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的开发者理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。
|
||||
|
||||
文档注释使用三斜杠 `///` 而不是两斜杆以支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释:
|
||||
文档注释使用三斜杠 `///` 而不是两斜杆以支持 Markdown 标记来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
|
@ -312,7 +312,7 @@ impl State for Published {
|
||||
|
||||
这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft` 和 `PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。
|
||||
|
||||
注意这个方法需要生命周期注解,如第 10 章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。
|
||||
注意这个方法需要生命周期标注,如第 10 章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。
|
||||
|
||||
现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
|
||||
|
||||
|
@ -261,7 +261,7 @@ fn main() {
|
||||
|
||||
> #### 从其它语言调用 Rust 函数
|
||||
>
|
||||
> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
|
||||
> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 标注来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
|
||||
>
|
||||
> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问:
|
||||
>
|
||||
|
@ -46,7 +46,7 @@ pub trait Iterator<T> {
|
||||
|
||||
<span class="caption">示例 19-13: 一个使用泛型的 `Iterator` trait 假想定义</span>
|
||||
|
||||
区别在于当如示例 19-13 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 `Iterator<String> for Counter`,或任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型注解来表明希望使用 `Iterator` 的哪一个实现。
|
||||
区别在于当如示例 19-13 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 `Iterator<String> for Counter`,或任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型标注来表明希望使用 `Iterator` 的哪一个实现。
|
||||
|
||||
通过关联类型,则无需标注类型,因为不能多次实现这个 trait。对于示例 19-12 使用关联类型的定义,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。
|
||||
|
||||
@ -366,7 +366,7 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 19-21: 使用完全限定语法来指定我们希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数</span>
|
||||
|
||||
我们在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 `Dog` 类型当作 `Animal` 对待,来指定希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数。现在这段代码会打印出我们期望的数据:
|
||||
我们在尖括号中向 Rust 提供了类型标注,并通过在此函数调用中将 `Dog` 类型当作 `Animal` 对待,来指定希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数。现在这段代码会打印出我们期望的数据:
|
||||
|
||||
```text
|
||||
A baby dog is called a puppy
|
||||
|
@ -39,7 +39,7 @@ println!("x + y = {}", x + y);
|
||||
Box<dyn Fn() + Send + 'static>
|
||||
```
|
||||
|
||||
在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-24 这样全是如此代码的项目:
|
||||
在函数签名或类型标注中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-24 这样全是如此代码的项目:
|
||||
|
||||
```rust
|
||||
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
|
||||
|
@ -55,8 +55,7 @@ macro_rules! vec {
|
||||
|
||||
> 注意:标准库中实际定义的 `vec!` 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
|
||||
|
||||
|
||||
无论何时导入定义了宏的包,`#[macro_export]` 注解说明宏应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
|
||||
无论何时导入定义了宏的包,`#[macro_export]` 标注说明宏应该是可用的。 如果没有该标注,这个宏不能被引入作用域。
|
||||
|
||||
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 **不带** 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec` 。
|
||||
|
||||
@ -114,7 +113,7 @@ pub fn some_name(input: TokenStream) -> TokenStream {
|
||||
|
||||
### 如何编写自定义 `derive` 宏
|
||||
|
||||
让我们创建一个 `hello_macro` crate,其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让 crate 的用户为其每一个类型实现 `HelloMacro` trait,我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate,使开发者能够写类似示例 19-30 中的代码。
|
||||
让我们创建一个 `hello_macro` crate,其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让 crate 的用户为其每一个类型实现 `HelloMacro` trait,我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 标注他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate,使开发者能够写类似示例 19-30 中的代码。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -235,7 +234,7 @@ pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
|
||||
|
||||
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
|
||||
|
||||
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了注解:`HelloMacro` ,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
|
||||
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了标注:`HelloMacro`,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
|
||||
|
||||
该函数首先将来自 `TokenStream` 的 `input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse_derive_input` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 19-32 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分:
|
||||
|
||||
@ -269,7 +268,7 @@ DeriveInput {
|
||||
|
||||
你可能也注意到了,当调用 `syn::parse` 函数失败时,我们用 `unwrap` 来使 `hello_macro_derive` 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!` 或 `expect` 来提供关于发生何种错误的更加明确的错误信息。
|
||||
|
||||
现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 19-33 所示。
|
||||
现在我们有了将标注的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注明类型上实现 `HelloMacro` trait 的代码,如示例 19-33 所示。
|
||||
|
||||
<span class="filename">文件名: hello_macro_derive/src/lib.rs</span>
|
||||
|
||||
@ -289,7 +288,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
|
||||
<span class="caption">示例 19-33: 使用解析过的 Rust 代码实现 `HelloMacro` trait</span>
|
||||
|
||||
我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 19-32 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-30 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-33 中 `name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-30 中结构体的名称。
|
||||
我们得到一个包含以 `ast.ident` 作为注明类型名字(标识符)的 `Ident` 结构体实例。示例 19-32 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-30 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-33 中 `name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-30 中结构体的名称。
|
||||
|
||||
`quote!` 宏让我们可以编写希望返回的 Rust 代码。`quote!` 宏执行的直接结果并不是编译器所期望的并需要转换为 `TokenStream`。为此需要调用 `into` 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 `TokenStream` 类型值。
|
||||
|
||||
@ -297,7 +296,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
|
||||
[quote-docs]: https://docs.rs/quote
|
||||
|
||||
我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注解类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和注解的类型名。
|
||||
我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注明类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和标注的类型名。
|
||||
|
||||
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` , 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!` 或 `println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!` 。 `stringify!` 编译时也保留了一份将 `#name` 转换为字符串之后的内存分配。
|
||||
|
||||
@ -315,7 +314,7 @@ hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
|
||||
|
||||
### 类属性宏
|
||||
|
||||
类属性宏与自定义派生宏相似,不同于为 `derive` 属性生成代码,它们允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架(web application framework)的函数:
|
||||
类属性宏与自定义派生宏相似,不同于为 `derive` 属性生成代码,它们允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于标注 web 应用程序框架(web application framework)的函数:
|
||||
|
||||
```rust,ignore
|
||||
#[route(GET, "/")]
|
||||
|
Loading…
Reference in New Issue
Block a user