check ch11-01

This commit is contained in:
KaiserY 2017-08-24 17:21:59 +08:00
parent 3ecdee1c8a
commit 61f0353848
4 changed files with 137 additions and 190 deletions

View File

@ -10,4 +10,4 @@
> ### 关键字 > ### 关键字
> >
> Rust 语言有一系列保留的 **关键字***keywords*),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有相应的宫娥能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。 > Rust 语言有一系列保留的 **关键字***keywords*),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有相应的能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。

View File

@ -2,9 +2,9 @@
> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md) > [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md)
> <br> > <br>
> commit 9fbbfb23c2cd1686dbd3ce7950ae1eda300937f6 > commit aa4be9389d18c31f587ebf75cbbb6af39ff4247d
当在第四章讨论引用时我们遗漏了一个重要的细节Rust 中的每一个引用都有其**生命周期**,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 当在第四章讨论引用时我们遗漏了一个重要的细节Rust 中的每一个引用都有其 **生命周期***lifetime*,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。 好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。
@ -12,7 +12,7 @@
### 生命周期避免了悬垂引用 ### 生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量`r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将`r`的值设置为一个`x`的引用。接着在内部作用域结束后,尝试打印出`r`的值: 生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下列表 10-18 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值:
```rust,ignore ```rust,ignore
{ {
@ -27,8 +27,7 @@
} }
``` ```
<span class="caption">Listing 10-16: An attempt to use a reference whose value <span class="caption">列表 10-18尝试使用离开作用域的值的引用</span>
has gone out of scope</span>
> ### 未初始化变量不能被使用 > ### 未初始化变量不能被使用
> >
@ -36,7 +35,7 @@ has gone out of scope</span>
当编译这段代码时会得到一个错误: 当编译这段代码时会得到一个错误:
``` ```text
error: `x` does not live long enough error: `x` does not live long enough
| |
6 | r = &x; 6 | r = &x;
@ -48,11 +47,11 @@ error: `x` does not live long enough
| - borrowed value needs to live until here | - borrowed value needs to live until here
``` ```
变量`x`并没有“存在的足够久”。为什么呢?好吧,`x`在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过`r`在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,`r`将会引用在`x`离开作用域时被释放的内存,这时尝试对`r`做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢? 变量 `x` 并没有 “存在的足够久”。为什么呢?好吧,`x` 在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过 `r` 在外部作用域也是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r`将会引用在`x`离开作用域时被释放的内存,这时尝试对`r`做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
#### 借用检查器 #### 借用检查器
编译器的这一部分叫做**借用检查器***borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释: 编译器的这一部分叫做 **借用检查器***borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-19 展示了与列表 10-18 相同的例子不过带有变量声明周期的注释:
```rust,ignore ```rust,ignore
{ {
@ -69,9 +68,7 @@ error: `x` does not live long enough
} }
``` ```
<span class="caption">Listing 10-17: Annotations of the lifetimes of `r` and <span class="caption">列表 10-19`r` 和 `x` 的生命周期注解,分别叫做 `'a``'b`</span>
`x`, named `'a` and `'b` respectively</span>
<!-- Just checking I'm reading this right: the inside block is the b lifetime, <!-- Just checking I'm reading this right: the inside block is the b lifetime,
correct? I want to leave a note for production, make sure we can make that correct? I want to leave a note for production, make sure we can make that
@ -81,9 +78,9 @@ line and ends with the first closing curly brace on the 7th line. Do you think
the text art comments work or should we make an SVG diagram that has nicer the text art comments work or should we make an SVG diagram that has nicer
looking arrows and labels? /Carol --> looking arrows and labels? /Carol -->
我们将`r`的声明周期标记为`'a`而将`x`的生命周期标记为`'b`。如你所见,内部的`'b`块要比外部的生命周期`'a`小得多。在编译时Rust 比较这两个生命周期的大小,并发现`r`拥有声明周期`'a`,不过它引用了一个拥有生命周期`'b`的对象。程序被拒绝编译,因为生命周期`'b`比生命周期`'a`要小:被引用的对象比它的引用者存活的时间更短。 我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时Rust 比较这两个生命周期的大小,并发现 `r` 拥有声明周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存活的时间更短。
让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子: 让我们看看列表 10-20 中这个并没有产生悬垂引用且可以正确编译的例子:
```rust ```rust
{ {
@ -96,18 +93,17 @@ looking arrows and labels? /Carol -->
} // -----+ } // -----+
``` ```
<span class="caption">Listing 10-18: A valid reference because the data has a <span class="caption">列表 10-20一个有效的引用因为数据比引用有着更长的生命周期</span>
longer lifetime than the reference</span>
`x`拥有生命周期 `'b`,在这里它比 `'a`要大。这就意味着`r`可以引用`x`Rust 知道`r`中的引用在`x`有效的时候也会一直有效 这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的
现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。 现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。
### 函数中的泛型生命周期 ### 函数中的泛型生命周期
让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了`longest`函数,列表 10-19 中的代码应该会打印出`The longest string is abcd` 让我们来编写一个返回两个字符串 slice 中较长者的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了 `longest` 函数,列表 10-21 中的代码应该会打印出 `The longest string is abcd`
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust,ignore ```rust,ignore
fn main() { fn main() {
@ -119,10 +115,9 @@ fn main() {
} }
``` ```
<span class="caption">Listing 10-19: A `main` function that calls the `longest` <span class="caption">列表 10-21`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span>
function to find the longest of two string slices</span>
注意函数期望获取字符串 slice如第四章所讲到的这是引用因为我们并不希望`longest`函数获取其参数的引用。我们希望函数能够接受`String`的 slice也就是变量`string1`的类型)和字符串字面值(也就是变量`string2`包含的值)。 注意函数期望获取字符串 slice如第四章所讲到的这是引用因为我们并不希望`longest` 函数获取其参数的所有权。我们希望函数能够接受 `String` 的 slice也就是变量 `string1` 的类型)以及字符串字面值(也就是变量 `string2` 包含的值)。
<!-- why is `a` a slice and `b` a literal? You mean "a" from the string "abcd"? --> <!-- why is `a` a slice and `b` a literal? You mean "a" from the string "abcd"? -->
<!-- I've changed the variable names to remove ambiguity between the variable <!-- I've changed the variable names to remove ambiguity between the variable
@ -141,11 +136,11 @@ and below). If these topics are confusing you in this context, I'd be
interested to know if rereading Chapter 4 clears up that confusion. interested to know if rereading Chapter 4 clears up that confusion.
/Carol --> /Carol -->
参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。 参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。
如果尝试像列表 10-20 中那样实现`longest`函数,它并不能编译: 如果尝试像列表 10-22 中那样实现 `longest` 函数,它并不能编译:
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust,ignore ```rust,ignore
fn longest(x: &str, y: &str) -> &str { fn longest(x: &str, y: &str) -> &str {
@ -157,13 +152,11 @@ fn longest(x: &str, y: &str) -> &str {
} }
``` ```
<span class="caption">Listing 10-20: An implementation of the `longest` <span class="caption">列表 10-22一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译</span>
function that returns the longest of two string slices, but does not yet
compile</span>
将会出现如下有关生命周期的错误: 将会出现如下有关生命周期的错误:
``` ```text
error[E0106]: missing lifetime specifier error[E0106]: missing lifetime specifier
| |
1 | fn longest(x: &str, y: &str) -> &str { 1 | fn longest(x: &str, y: &str) -> &str {
@ -173,17 +166,17 @@ error[E0106]: missing lifetime specifier
signature does not say whether it is borrowed from `x` or `y` signature does not say whether it is borrowed from `x` or `y`
``` ```
提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向`x`或`y`。事实上我们也不知道,因为函数体中`if`块返回一个`x`的引用而`else`块返回一个`y`的引用。 提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用。
虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是`if`还是`else`会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-17 和 10-18 那样通过观察作用域来确定返回的引用总是有效。借用检查器自身同样也无法确定,因为它不知道`x`和`y`的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行相关分析。 虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-19 和 10-20 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` `y` 的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
### 生命周期注解语法 ### 生命周期注解语法
生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。 生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。`'a`是大多数人默认使用的名称。生命周期参数注解位于引用的`&`之后,并有一个空格来将引用类型与生命周期注解分隔开。 生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。
这里有一些例子:我们有一个没有生命周期参数的`i32`的引用,一个有叫做`'a`的生命周期参数的`i32`的引用,和一个也有的生命周期参数`'a`的`i32`的可变引用: 这里有一些例子:我们有一个没有生命周期参数的 `i32` 的引用,一个有叫做 `'a` 的生命周期参数的 `i32` 的引用,和一个也有的生命周期参数 `'a` `i32` 的可变引用:
```rust,ignore ```rust,ignore
&i32 // a reference &i32 // a reference
@ -191,13 +184,13 @@ error[E0106]: missing lifetime specifier
&'a mut i32 // a mutable reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime
``` ```
生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期`'a`的`i32`的引用的参数`first`,还有另一个同样是生命周期`'a`的`i32`的引用的参数`second`,这两个生命周期注解有相同的名称意味着`first`和`second`必须与这相同的泛型生命周期存在得一样久。 生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期 `'a` `i32` 的引用的参数 `first`,还有另一个同样是生命周期 `'a` `i32` 的引用的参数 `second`,这两个生命周期注解有相同的名称意味着 `first` `second` 必须与这相同的泛型生命周期存在得一样久。
### 函数签名中的生命周期注解 ### 函数签名中的生命周期注解
来看看我们编写的`longest`函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了`'a`那样: 来看看我们编写的 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-23 中在每个引用中都加上了 `'a` 那样:
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust ```rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
@ -209,23 +202,21 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
} }
``` ```
<span class="caption">Listing 10-21: The `longest` function definition that <span class="caption">列表 10-23`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span>
specifies all the references in the signature must have the same lifetime,
`'a`</span>
这段代码能够编译并会产生我们想要使用列表 10-19 中的`main`函数得到的结果。 这段代码能够编译并会产生我们想要使用列表 10-21 中的 `main` 函数得到的结果。
现在函数签名表明对于某些生命周期`'a`,函数会获取两个参数,他们都是与生命周期`'a`存在的一样长的字符串 slice。函数会返回一个同样也与生命周期`'a`存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。 现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。
通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x`和`y`具体会存在多久,不过只需要知道一些可以使用`'a`替代的作用域将会满足这个签名。 通过在函数签名中指定生命周期参数,不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x` `y` 具体会存在多久,不过只需要知道一些可以使用 `'a` 替代的作用域将会满足这个签名。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。 当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。
当具体的引用被传递给`longest`时,具体被`'a`所替代的生命周期是`x`的作用域与`y`的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期`'a`的具体生命周期等同于`x`和`y`的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在`x`和`y`中较短的那个生命周期结束之前保持有效。 当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在 `x` `y` 中较短的那个生命周期结束之前保持有效。
让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制`longest`函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:`string1`直到外部作用域结束都是有效的,`string2`则在内部作用域中是有效的,而`result`则引用了一些直到内部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出`The longest string is long string is long` 让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制 `longest` 函数的使用的。列表 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust ```rust
# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
@ -247,12 +238,11 @@ fn main() {
} }
``` ```
<span class="caption">Listing 10-22: Using the `longest` function with <span class="caption">列表 10-24通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span>
references to `String` values that have different concrete lifetimes</span>
接下来,让我们尝试一个`result`的引用的生命周期必须比两个参数的要短的例子。将`result`变量的声明从内部作用域中移动出来,不过将`result`和`string2`变量的赋值语句一同放在内部作用域里。接下来,我们将使用`result`的`println!`移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译: 接下来,让我们尝试一个 `result` 的引用的生命周期必须比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,不过将 `result` `string2` 变量的赋值语句一同放在内部作用域里。接下来,我们将使用 `result` `println!` 移动到内部作用域之外,就在其结束之后。注意列表 10-25 中的代码不能编译:
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust,ignore ```rust,ignore
fn main() { fn main() {
@ -266,12 +256,11 @@ fn main() {
} }
``` ```
<span class="caption">Listing 10-23: Attempting to use `result` after `string2` <span class="caption">列表 10-25`string2` 离开作用域之后使用 `result` 的尝试不能编译</span>
has gone out of scope won't compile</span>
如果尝试编译会出现如下错误: 如果尝试编译会出现如下错误:
``` ```text
error: `string2` does not live long enough error: `string2` does not live long enough
| |
6 | result = longest(string1.as_str(), string2.as_str()); 6 | result = longest(string1.as_str(), string2.as_str());
@ -283,17 +272,17 @@ error: `string2` does not live long enough
| - borrowed value needs to live until here | - borrowed value needs to live until here
``` ```
错误表明为了保证`println!`中的`result`是有效的,`string2`需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数`'a`。 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`
以我们的理解`string1`更长,因此`result`会包含指向`string1`的引用。因为`string1`还未离开作用域,对于`println!`来说`string1`的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是`longest`函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许列表 10-23 中的代码,因为它可能会存在无效的引用。 以我们的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许列表 10-25 中的代码,因为它可能会存在无效的引用。
请尝试更多采用不同的值和不同生命周期的引用作为`longest`函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你是否正确 请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确!
### 深入理解生命周期 ### 深入理解生命周期
指定生命周期参数的正确方式依赖函数具体的功能。例如,如果将`longest`函数的实现修改为总是返回第一个参数而不是最长的字符串 slice就不需要为参数`y`指定一个生命周期。如下代码将能够编译: 指定生命周期参数的正确方式依赖函数具体的功能。例如,如果将 `longest` 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice就不需要为参数 `y` 指定一个生命周期。如下代码将能够编译:
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust ```rust
fn longest<'a>(x: &'a str, y: &str) -> &'a str { fn longest<'a>(x: &'a str, y: &str) -> &'a str {
@ -301,11 +290,11 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str {
} }
``` ```
在这个例子中,我们为参数`x`和返回值指定了生命周期参数`'a`,不过没有为参数`y`指定,因为`y`的生命周期与参数`x`和返回值的生命周期没有任何关系。 在这个例子中,我们为参数 `x` 和返回值指定了生命周期参数 `'a`,不过没有为参数 `y` 指定,因为 `y` 的生命周期与参数 `x` 和返回值的生命周期没有任何关系。
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用**没有**指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的`longest`函数实现: 当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 **没有** 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 `longest` 函数实现:
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust,ignore ```rust,ignore
fn longest<'a>(x: &str, y: &str) -> &'a str { fn longest<'a>(x: &str, y: &str) -> &'a str {
@ -314,9 +303,9 @@ fn longest<'a>(x: &str, y: &str) -> &'a str {
} }
``` ```
即便我们为返回值指定了生命周期参数`'a`,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息: 即便我们为返回值指定了生命周期参数 `'a`,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息:
``` ```text
error: `result` does not live long enough error: `result` does not live long enough
| |
3 | result.as_str() 3 | result.as_str()
@ -331,15 +320,15 @@ at 1:44...
| ^ | ^
``` ```
出现的问题是`result`在函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个`result`的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。 出现的问题是 `result` `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
从结果上看生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。 从结果上看生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
### 结构体定义中的生命周期注解 ### 结构体定义中的生命周期注解
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体`ImportantExcerpt` 目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-26 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`
<span class="filename">Filename: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust ```rust
struct ImportantExcerpt<'a> { struct ImportantExcerpt<'a> {
@ -355,18 +344,17 @@ fn main() {
} }
``` ```
<span class="caption">Listing 10-24: A struct that holds a reference, so its <span class="caption">列表 10-26一个存放引用的结构体所以其定义需要生命周期注解</span>
definition needs a lifetime annotation</span>
这个结构体有一个字段,`part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。 这个结构体有一个字段,`part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。
这里的`main`函数创建了一个`ImportantExcerpt`的实例,它存放了变量`novel`所拥有的`String`的第一个句子的引用。 这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。
### 生命周期省略 ### 生命周期省略
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译: 在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在列表 10-27 中再次展示出来,它没有生命周期注解却能成功编译:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
```rust ```rust
fn first_word(s: &str) -> &str { fn first_word(s: &str) -> &str {
@ -382,11 +370,9 @@ fn first_word(s: &str) -> &str {
} }
``` ```
<span class="caption">Listing 10-25: A function we defined in Chapter 4 that <span class="caption">列表 10-27第四章定义了一个没有使用生命周期注解的函数即便其参数和返回值都是引用</span>
compiled without lifetime annotations, even though the parameter and return
type are references</span>
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: 这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
```rust,ignore ```rust,ignore
fn first_word<'a>(s: &'a str) -> &'a str { fn first_word<'a>(s: &'a str) -> &'a str {
@ -394,29 +380,29 @@ 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 引用分析的模式被称为 **生命周期省略规则***lifetime elision rules*)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
这些规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。 省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。
首先,介绍一些定义:函数或方法的参数的生命周期被称为**输入生命周期***input lifetimes*),而返回值的生命周期被称为**输出生命周期***output lifetimes*)。 首先,介绍一些定义:函数或方法的参数的生命周期被称为 **输入生命周期***input lifetimes*),而返回值的生命周期被称为 **输出生命周期***output lifetimes*)。
现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。 现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。
1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故是`&self`或`&mut self`,那么`self`的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。 3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故`&self``&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。
假设我们自己就是编译器并来计算列表 10-25 `first_word`函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: 假设我们自己就是编译器并来计算列表 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
```rust,ignore ```rust,ignore
fn first_word(s: &str) -> &str { fn first_word(s: &str) -> &str {
``` ```
接着我们(作为编译器)应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为`'a`,所以现在签名看起来像这样: 接着我们(作为编译器)应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 `'a`,所以现在签名看起来像这样:
```rust,ignore ```rust,ignore
fn first_word<'a>(s: &'a str) -> &str { fn first_word<'a>(s: &'a str) -> &str {
@ -430,19 +416,19 @@ fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。 现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
让我们再看看另一个例子,这次我们从列表 10-20 中没有生命周期参数的`longest`函数开始: 让我们再看看另一个例子,这次我们从列表 10-22 中没有生命周期参数的 `longest` 函数开始:
```rust,ignore ```rust,ignore
fn longest(x: &str, y: &str) -> &str { fn longest(x: &str, y: &str) -> &str {
``` ```
再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所就有两个生命周期: 再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所就有两个生命周期:
```rust,ignore ```rust,ignore
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
``` ```
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有`self`参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译列表 10-20 的代码时会出现错误的原因:编译器适用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。 再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译列表 10-22 的代码时会出现错误的原因:编译器适用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。 因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
@ -456,13 +442,13 @@ parameters need to be declared and used since the lifetime parameters could go
with the struct's fields or with references passed into or returned from with the struct's fields or with references passed into or returned from
methods. /Carol --> methods. /Carol -->
当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和生命周期参数是否与结构体字段或方法的参数与返回值相关联。 当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-11 中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关
(实现方法时)结构体字段的生命周期必须总是在`impl`关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。 (实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
`impl`块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-24 中定义的结构体`ImportantExcerpt`的例子。 `impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-26 中定义的结构体 `ImportantExcerpt` 的例子。
首先,这里有一个方法`level`。其唯一的参数是`self`的引用,而且返回值只是一个`i32`,并不引用任何值: 首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值:
```rust ```rust
# struct ImportantExcerpt<'a> { # struct ImportantExcerpt<'a> {
@ -476,7 +462,7 @@ impl<'a> ImportantExcerpt<'a> {
} }
``` ```
`impl`之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注`self`引用的生命周期。 `impl` 之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 `self` 引用的生命周期。
这里是一个适用于第三条生命周期省略规则的例子: 这里是一个适用于第三条生命周期省略规则的例子:
@ -493,22 +479,22 @@ impl<'a> ImportantExcerpt<'a> {
} }
``` ```
这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予`&self`和`announcement`他们各自的生命周期。接着,因为其中一个参数是`&self`,返回值类型被赋予了`&self`的生命周期,这样所有的生命周期都被计算出来了。 这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 `&self` `announcement` 他们各自的生命周期。接着,因为其中一个参数是 `&self`,返回值类型被赋予了 `&self` 的生命周期,这样所有的生命周期都被计算出来了。
### 静态生命周期 ### 静态生命周期
这里有**一种**特殊的生命周期值得讨论:`'static`。`'static`生命周期存活于整个程序期间。所有的字符串字面值都拥有`'static`生命周期,我们也可以选择像下面这样标注出来: 这里有 **一种** 特殊的生命周期值得讨论:`'static`。`'static` 生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来:
```rust ```rust
let s: &'static str = "I have a static lifetime."; let s: &'static str = "I have a static lifetime.";
``` ```
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是`'static`的。 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 `'static` 的。
<!-- How would you add a static lifetime (below)? --> <!-- How would you add a static lifetime (below)? -->
<!-- Just like you'd specify any lifetime, see above where it shows `&'static str`. /Carol --> <!-- Just like you'd specify any lifetime, see above where it shows `&'static str`. /Carol -->
你可能在错误信息的帮助文本中见过使用`'static`生命周期的建议,不过将引用指定为`'static`之前,思考一下这个引用是否真的在整个程序的生命周期里都有效(或者哪怕你希望它一直有效,如果可能的话)。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个`'static`的生命周期。 你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效(或者哪怕你希望它一直有效,如果可能的话)。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。
### 结合泛型类型参数、trait bounds 和生命周期 ### 结合泛型类型参数、trait bounds 和生命周期
@ -529,10 +515,10 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st
} }
``` ```
这个是列表 10-21 中那个返回两个字符串 slice 中最长者的`longest`函数,不过带有一个额外的参数`ann`。`ann`的类型是泛型`T`,它可以被放入任何实现了`where`从句中指定的`Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么`Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数`'a`和泛型类型参数`T`都位于函数名后的同一尖括号列表中。 这个是列表 10-23 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。
## 总结 ## 总结
这一章介绍了很多的内容现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率! 这一章介绍了很多的内容现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!
你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! 你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!

View File

@ -2,7 +2,7 @@
> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md) > [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md)
> <br> > <br>
> commit b7ab6668bbcb73b93c6464d8354c94a8e6c90395 > commit 1047433147b27d19e5acc068a0ebca5782d64f99
> Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence. > Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
> >
@ -12,10 +12,10 @@
> >
> Edsger W. Dijkstra【谦卑的程序员】1972 > Edsger W. Dijkstra【谦卑的程序员】1972
程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此Rust 也包含为语言自身编写软件测试的支持。 程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此Rust 也在语言本身包含了编写软件测试的支持。
例如,我们可以编写一个叫做`add_two`的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递`String`或无效的引用给这个函数。Rust 所**不能**检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。 例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时Rust 会进行所有目前我们已经见过的的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
我们可以编写测试断言,比如说,当传递`3`给`add_two`函数时,应该得到`5`。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。 我们可以编写测试断言,比如说,当传递 `3` `add_two` 函数时,应该得到 `5`。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。
测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。 测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。

View File

@ -2,39 +2,29 @@
> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md) > [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md)
> <br> > <br>
> commit c6162d22288253b2f2a017cfe96cf1aa765c2955 > commit db08b34db5f1c78b4866b391c802344ec94ecc38
测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:`test`属性、一些宏和`should_panic`属性。 测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的专门用来编写测试的功能:`test` 属性、一些宏和 `should_panic` 属性。
### 测试函数剖析 ### 测试函数剖析
作为最简单例子Rust 中的测试就是一个带有`test`属性注解的函数。属性attribute是关于 Rust 代码片段的元数据:第五章中结构体中用到的`derive`属性就是一个例子。为了将一个函数变成测试函数,需要在`fn`行之前加上`#[test]`。当使用`cargo test`命令运行测试函数时Rust 会构建一个测试执行者二进制文件用来运行标记了`test`属性的函数并报告每一个测试是通过还是失败。 作为最简单例子Rust 中的测试就是一个带有 `test` 属性注解的函数。属性attribute是关于 Rust 代码片段的元数据:第五章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试函数时Rust 会构建一个测试执行者二进制文件用来运行标记了 `test` 属性的函数并报告每一个测试是通过还是失败。
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然也可以额外增加任意多的测试函数以及测试模块!
<!-- is it annotated with `test` by the user, or only automatically? I think 我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作方面的一些内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是否正确。
it's the latter, and has edited with a more active tone to make that clear, but
please change if I'm wrong -->
<!-- What do you mean by "only automatically"? The reader should be typing in
`#[test] on their own when they add new test functions; there's nothing special
about that text. I'm not sure what part of this chapter implied "only
automatically", can you point out where that's happening if we haven't taken
care of it? /Carol -->
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。同时可以额外增加任意多的测试函数以及测试模块! 让我们创建一个新的库项目 `adder`
我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作的一些方面内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是正确的。 ```text
让我们创建一个新的库项目`adder`
```
$ cargo new adder $ cargo new adder
Created library `adder` project Created library `adder` project
$ cd adder $ cd adder
``` ```
adder 库中`src/lib.rs`的内容应该看起来像这样: adder 库中 `src/lib.rs` 的内容应该看起来像这样:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
```rust ```rust
#[cfg(test)] #[cfg(test)]
@ -45,19 +35,18 @@ mod tests {
} }
``` ```
<span class="caption">Listing 11-1: The test module and function generated <span class="caption">列表 11-1`cargo new` 自动生成的测试模块和函数</span>
automatically for us by `cargo new` </span>
现在让我们暂时忽略`tests`模块和`#[cfg(test)]`注解并只关注函数。注意`fn`行之前的`#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在`tests`模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用`#[test]`属性标明哪些函数是测试。 现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。 这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。
`cargo test`命令会运行项目中所有的测试,如列表 11-2 所示: `cargo test` 命令会运行项目中所有的测试,如列表 11-2 所示:
``` ```text
$ cargo test $ cargo test
Compiling adder v0.1.0 (file:///projects/adder) Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs Finished dev [unoptimized + debuginfo] target(s) in 0.22 secs
Running target/debug/deps/adder-ce99bcc2479f4607 Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test running 1 test
@ -72,25 +61,15 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
``` ```
<span class="caption">Listing 11-2: The output from running the one <span class="caption">列表 11-2运行自动生成测试的输出</span>
automatically generated test </span>
Cargo 编译并运行了测试。在`Compiling`、`Finished`和`Running`这几行之后,可以看到`running 1 test`这一行。下一行显示了生成的测试函数的名称,它是`it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.`意味着所有测试都通过了。`1 passed; 0 failed`表示通过或失败的测试数量。 Cargo 编译并运行了测试。在 `Compiling`、`Finished` `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。
这里并没有任何被标记为忽略的测试,所以总结表明`0 ignored`。在下一部分关于运行测试的不同方式中会讨论忽略测试。`0 measured`统计是针对测试性能的性能测试的。性能测试benchmark tests在编写本书时仍只属于开发版 Rustnightly Rust。请查看附录 D 来了解更多开发版 Rust 的信息。 这里并没有任何被标记为忽略的测试,所以总结表明 `0 ignored`。在下一部分关于运行测试的不同方式中会讨论忽略测试。`0 measured` 统计是针对测试性能的性能测试的。性能测试benchmark tests在编写本书时仍只能用于 Rust 开发版nightly Rust。请查看附录 D 来了解更多 Rust 开发版的信息。
测试输出中以`Doc-tests adder`开头的下一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的“文档注释”部分会讲到如何编写文档测试。现在我们将忽略`Doc-tests`部分的输出。 测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
<!-- I might suggest changing the name of the function, could be misconstrued 让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
as part of the test output! -->
<!-- `it_works` is always the name that `cargo new` generates for the first
test function, though. We wanted to show the reader what happens when you run
the tests immediately after generating a new project; they pass without you
needing to change anything. I've added a bit to walk through changing the
function name and seeing how the output changes; I hope that's sufficient.
/Carol -->
让我们改变测试的名称并看看这如何改变测试的输出。给`it_works`函数起个不同的名字,比如`exploration`,像这样:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
@ -103,18 +82,18 @@ mod tests {
} }
``` ```
并再次运行`cargo test`。现在输出中将出现`exploration`而不是`it_works` 并再次运行 `cargo test`。现在输出中将出现 `exploration` 而不是 `it_works`
``` ```text
running 1 test running 1 test
test tests::exploration ... ok test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
``` ```
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。第九章讲到了最简单的造成 panic 的方法:调用`panic!`宏!写入新函数后 `src/lib.rs` 现在看起来如列表 11-3 所示: 让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏!写入新函数后 `src/lib.rs` 现在看起来如列表 11-3 所示:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
```rust ```rust
#[cfg(test)] #[cfg(test)]
@ -130,12 +109,10 @@ mod tests {
} }
``` ```
<span class="caption">Listing 11-3: Adding a second test; one that will fail <span class="caption">列表 11-3增加第二个测试他会失败因为调用了 `panic!`</span>
since we call the `panic!` macro </span>
再次`cargo test`运行测试。输出应该看起来像列表 11-4它表明`exploration`测试通过了而`another`失败了: 再次 `cargo test` 运行测试。输出应该看起来像列表 11-4它表明 `exploration` 测试通过了而 `another` 失败了:
```text ```text
running 2 tests running 2 tests
@ -156,35 +133,21 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed error: test failed
``` ```
<span class="caption">Listing 11-4: Test results when one test passes and one <span class="caption">列表 11-4一个测试通过和一个测试失败的测试结果</span>
test fails </span>
`test tests::another`这一行是`FAILED`而不是`ok`了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another`因为`panicked at 'Make this test fail'`而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。 `test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。
最后是总结行:总体上讲,一个测试结果是`FAILED`的。有一个测试通过和一个测试失败。 最后是总结行:总体上讲,一个测试结果是 `FAILED` 的。有一个测试通过和一个测试失败。
现在我们见过不同场景中测试结果是什么样子的了,再来看看除了`panic!`之外一些在测试中有帮助的宏吧。 现在我们见过不同场景中测试结果是什么样子的了,再来看看除 `panic!` 之外的一些在测试中有帮助的宏吧。
### 使用`assert!`宏来检查结果 ### 使用 `assert!` 宏来检查结果
`assert!`宏由标准库提供,在希望确保测试中一些条件为`true`时非常有用。需要向`assert!`宏提供一个计算为布尔值的参数。如果值是`true``assert!`什么也不做同时测试会通过。如果值为`false``assert!`调用`panic!`宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。 `assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true``assert!` 什么也不做同时测试会通过。如果值为 `false``assert!` 调用 `panic!` 宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。
<!-- what kind of thing can be passed as an argument? Presumably when we use it 回忆一下第五章中,列表 5-9 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在列表 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用 `assert!` 宏编写一些测试。
for real we won't pass it `true` or `false` as an argument, but some condition
that will evaluate to true or false? In which case, should below be phrased "If
the argument evaluates to true" and an explanation of that? Or maybe even a
working example would be better, this could be misleading -->
<!-- We were trying to really break it down, to show just how the `assert!`
macro works and what it looks like for it to pass or fail, before we got into
calling actual code. We've changed this section to move a bit faster and just
write actual tests instead. /Carol -->
回忆一下第五章中,列表 5-9 中有一个`Rectangle`结构体和一个`can_hold`方法,在列表 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用`assert!`宏编写一些测试。 <span class="filename">文件名: src/lib.rs</span>
<!-- Listing 5-9 wasn't marked as such; I'll fix it the next time I get Chapter
5 for editing. /Carol -->
<span class="filename">Filename: src/lib.rs</span>
```rust ```rust
#[derive(Debug)] #[derive(Debug)]
@ -200,12 +163,11 @@ impl Rectangle {
} }
``` ```
<span class="caption">Listing 11-5: The `Rectangle` struct and its `can_hold` <span class="caption">列表 11-5第五章中 `Rectangle` 结构体和其 `can_hold` 方法</span>
method from Chapter 5 </span>
`can_hold`方法返回一个布尔值,这意味着它完美符合`assert!`宏的使用场景。在列表 11-6 中,让我们编写一个`can_hold`方法的测试来作为练习,这里创建一个长为 8 宽为 7 的`Rectangle`实例并假设它可以放得下另一个长为5 宽为 1 的`Rectangle`实例: `can_hold` 方法返回一个布尔值,这意味着它完美符合 `assert!` 宏的使用场景。在列表 11-6 中,让我们编写一个 `can_hold` 方法的测试来作为练习,这里创建一个长为 8 宽为 7 的 `Rectangle` 实例,并假设它可以放得下另一个长为 5 宽为 1 的 `Rectangle` 实例:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
```rust ```rust
#[cfg(test)] #[cfg(test)]
@ -222,14 +184,13 @@ mod tests {
} }
``` ```
<span class="caption">Listing 11-6: A test for `can_hold` that checks that a <span class="caption">列表 11-6一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
larger rectangle indeed holds a smaller rectangle </span>
注意在`tests`模块中新增加了一行:`use super::*;`。`tests`是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在`tests`模块中都是可用的。 注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在 `tests` 模块中都是可用的。
我们将测试命名为`larger_can_hold_smaller`,并创建所需的两个`Rectangle`实例。接着调用`assert!`宏并传递`larger.can_hold(&smaller)`调用的结果作为参数。这个表达式预期会返回`true`,所以测试应该通过。让我们拭目以待! 我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待!
``` ```text
running 1 test running 1 test
test tests::larger_can_hold_smaller ... ok test tests::larger_can_hold_smaller ... ok
@ -238,7 +199,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形: 它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
```rust ```rust
#[cfg(test)] #[cfg(test)]
@ -254,7 +215,7 @@ mod tests {
} }
#[test] #[test]
fn smaller_can_hold_larger() { fn smaller_cannot_hold_larger() {
let larger = Rectangle { length: 8, width: 7 }; let larger = Rectangle { length: 8, width: 7 };
let smaller = Rectangle { length: 5, width: 1 }; let smaller = Rectangle { length: 5, width: 1 };
@ -263,17 +224,17 @@ mod tests {
} }
``` ```
因为这里`can_hold`函数的正确结果是`false`,我们需要将这个结果取反后传递给`assert!`宏。这样的话,测试就会通过而`can_hold`将返回`false` 因为这里 `can_hold` 函数的正确结果是 `false`,我们需要将这个结果取反后传递给 `assert!` 宏。这样的话,测试就会通过而 `can_hold` 将返回`false`
``` ```text
running 2 tests running 2 tests
test tests::smaller_can_hold_larger ... ok test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... ok test tests::larger_can_hold_smaller ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
``` ```
个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将`can_hold`方法中比较长度时本应使用大于号的地方改成小于号: 个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将 `can_hold` 方法中比较长度时本应使用大于号的地方改成小于号:
```rust ```rust
#[derive(Debug)] #[derive(Debug)]
@ -291,9 +252,9 @@ impl Rectangle {
现在运行测试会产生: 现在运行测试会产生:
``` ```text
running 2 tests running 2 tests
test tests::smaller_can_hold_larger ... ok test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... FAILED test tests::larger_can_hold_smaller ... FAILED
failures: failures:
@ -309,9 +270,9 @@ failures:
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
``` ```
我们的测试捕获了 bug因为`larger.length`是 8 而`smaller.length` 是 5`can_hold`中的长度比较现在返回`false`因为 8 不小于 5。 我们的测试捕获了 bug因为 `larger.length` 是 8 而 `smaller.length` 是 5`can_hold` 中的长度比较现在因为 8 不小于 5 而返回 `false`
### 使用`assert_eq!`和`assert_ne!`宏来测试相等 ### 使用 `assert_eq!` `assert_ne!` 宏来测试相等
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向`assert!`宏传递一个使用`==`宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:`assert_eq!`和`assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试**为什么**失败,而`assert!`只会打印出它从`==`表达式中得到了`false`值,而不是导致`false`值的原因。 测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向`assert!`宏传递一个使用`==`宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:`assert_eq!`和`assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试**为什么**失败,而`assert!`只会打印出它从`==`表达式中得到了`false`值,而不是导致`false`值的原因。