From b58a6ce83ad225fcf6062a43af7325a9e3c93f54 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Tue, 17 Jan 2023 23:14:26 +0800 Subject: [PATCH] update ch10-03 close #677 --- src/ch10-02-traits.md | 51 ++------------ src/ch10-03-lifetime-syntax.md | 119 +++++++++++++++++---------------- 2 files changed, 67 insertions(+), 103 deletions(-) diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index 730c325..2349af0 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -2,7 +2,7 @@ > [ch10-02-traits.md](https://github.com/rust-lang/book/blob/main/src/ch10-02-traits.md) >
-> commit 3c2ca8528c3b92b7d30e73f2e8a1b84b2f68b0c8 +> commit 92bfbfacf88ee9a814cea0a58e9c019c529ef4ae *trait* 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。 @@ -167,10 +167,7 @@ fn some_function(t: &T, u: &U) -> i32 { 还可以像这样使用 `where` 从句: ```rust,ignore -fn some_function(t: &T, u: &U) -> i32 - where T: Display + Clone, - U: Clone + Debug -{ +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-where-clause/src/lib.rs:here}} ``` 这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。 @@ -195,51 +192,15 @@ fn some_function(t: &T, u: &U) -> i32 这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。第十七章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。 -### 使用 trait bounds 来修复 `largest` 函数 - -现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!回顾一下,最后尝试编译代码时出现的错误是: - -```console -{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/output.txt}} -``` - -在 `largest` 函数体中我们想要使用大于运算符(`>`)比较两个 `T` 类型的值。这个运算符被定义为标准库中 trait `std::cmp::PartialOrd` 的一个默认方法。所以需要在 `T` 的 trait bound 中指定 `PartialOrd`,这样 `largest` 函数可以用于任何可以比较大小的类型的 slice。因为 `PartialOrd` 位于 prelude 中所以并不需要手动将其引入作用域。将 `largest` 的签名修改为如下: - -```rust,ignore -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-fixing-listing-10-05/src/main.rs:here}} -``` - -但是如果编译代码的话,会出现一些不同的错误: - -```console -{{#include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-fixing-listing-10-05/output.txt}} -``` - -错误的核心是 `cannot move out of type [T], a non-copy slice`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章 [“只在栈上的数据:拷贝”][stack-only-data-copy] 部分讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的。这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中,这导致了上面的错误。 - -为了只对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-15 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` **和** `Copy` 这两个 trait,例如 `i32` 和 `char`: - -文件名:src/main.rs - -```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-15/src/main.rs}} -``` - -示例 10-15:一个可以用于任何实现了 `PartialOrd` 和 `Copy` trait 的泛型的 `largest` 函数 - -如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`。并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。使用 `clone` 函数意味着对于类似 `String` 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。 - -另一种 `largest` 的实现方式是返回在 slice 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!如果你无法摆脱与生命周期有关的错误,请继续阅读:接下来的 “生命周期与引用有效性” 部分会详细的说明,不过生命周期对于解决这些挑战来说并不是必须的。 - ### 使用 trait bound 有条件地实现方法 -通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法并返回一个 `Pair` 的实例(回忆一下第五章的 ["定义方法"][methods] 部分,`Self` 是一个 `impl` 块类型的类型别名(type alias),在这里是 `Pair`)。不过在下一个 `impl` 块中,只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair` 才会实现 `cmp_display` 方法: +通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-15 中的类型 `Pair` 总是实现了 `new` 方法并返回一个 `Pair` 的实例(回忆一下第五章的 ["定义方法"][methods] 部分,`Self` 是一个 `impl` 块类型的类型别名(type alias),在这里是 `Pair`)。不过在下一个 `impl` 块中,只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair` 才会实现 `cmp_display` 方法: ```rust,noplayground -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/src/lib.rs}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-15/src/lib.rs}} ``` -示例 10-16:根据 trait bound 在泛型上有条件的实现方法 +示例 10-15:根据 trait bound 在泛型上有条件的实现方法 也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样: @@ -259,8 +220,6 @@ blanket implementation 会出现在 trait 文档的 “Implementers” 部分。 trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。 -[stack-only-data-copy]: -ch04-01-what-is-ownership.html#只在栈上的数据拷贝 [using-trait-objects-that-allow-for-values-of-different-types]: ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象 [methods]: ch05-03-method-syntax.html#定义方法 diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index 90e7d99..a36ad89 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -2,7 +2,9 @@ > [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/main/src/ch10-03-lifetime-syntax.md) >
-> commit 1b8746013079f2e2ce1c8e85f633d9769778ea7f +> commit 5f67eee42345ba44f6f08a22c2192165f4b0e930 + +生命周期是另一类我们已经使用过的泛型。不同于确保类型有期望的行为,生命周期确保引用如预期一直有效。 当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 @@ -10,44 +12,43 @@ ### 生命周期避免了悬垂引用 -生命周期的主要目标是避免悬垂引用,后者会导致程序引用了非预期引用的数据。考虑一下示例 10-17 中的程序,它有一个外部作用域和一个内部作用域。 +生命周期的主要目标是避免**悬垂引用**(*dangling references*),后者会导致程序引用了非预期引用的数据。考虑一下示例 10-16 中的程序,它有一个外部作用域和一个内部作用域。 ```rust,ignore,does_not_compile -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/src/main.rs}} ``` -示例 10-17:尝试使用离开作用域的值的引用 +示例 10-16:尝试使用离开作用域的值的引用 -> 注意:示例 10-17、10-18 和 10-24 中声明了没有初始值的变量,所以这些变量存在于外部作用域。这乍看之下好像和 Rust 不允许存在空值相冲突。然而如果尝试在给它一个值之前使用这个变量,会出现一个编译时错误,这就说明了 Rust 确实不允许空值。 +> 注意:示例 10-16、10-17 和 10-23 中声明了没有初始值的变量,所以这些变量存在于外部作用域。这乍看之下好像和 Rust 不允许存在空值相冲突。然而如果尝试在给它一个值之前使用这个变量,会出现一个编译时错误,这就说明了 Rust 确实不允许空值。 外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值。这段代码不能编译因为 `r` 引用的值在尝试使用之前就离开了作用域。如下是错误信息: ```console -{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/output.txt}} +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/output.txt}} ``` 变量 `x` 并没有 “存在的足够久”。其原因是 `x` 在到达第 7 行内部作用域结束时就离开了作用域。不过 `r` 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?这得益于借用检查器。 #### 借用检查器 -Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-18 展示了与示例 10-17 相同的例子不过带有变量生命周期的注释: +Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-17 展示了与示例 10-16 相同的例子不过带有变量生命周期的注释: ```rust,ignore,does_not_compile -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-18/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/src/main.rs}} ``` -示例 10-18:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b` +示例 10-17:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b` 这里将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。 -让我们看看示例 10-19 中这个并没有产生悬垂引用且可以正确编译的例子: +让我们看看示例 10-18 中这个并没有产生悬垂引用且可以正确编译的例子: ```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-19/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-18/src/main.rs}} ``` - -示例 10-19:一个有效的引用,因为数据比引用有着更长的生命周期 +示例 10-18:一个有效的引用,因为数据比引用有着更长的生命周期 这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`:Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。 @@ -55,43 +56,43 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 ### 函数中的泛型生命周期 -让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-20 中的代码应该会打印出 `The longest string is abcd`: +让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-19 中的代码应该会打印出 `The longest string is abcd`: 文件名:src/main.rs ```rust,ignore -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/src/main.rs}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-19/src/main.rs}} ``` -示例 10-20:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个 +示例 10-19:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个 -注意这个函数获取作为引用的字符串 slice,因为我们不希望 `longest` 函数获取参数的所有权。参考之前第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。 +注意这个函数获取作为引用的字符串 slice,而不是字符串,因为我们不希望 `longest` 函数获取参数的所有权。参考之前第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-19 的参数正符合我们期望的讨论。 -如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译: +如果尝试像示例 10-20 中那样实现 `longest` 函数,它并不能编译: 文件名:src/main.rs ```rust,ignore,does_not_compile -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/src/main.rs:here}} ``` -示例 10-21:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译 +示例 10-20:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译 相应地会出现如下有关生命周期的错误: ```console -{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/output.txt}} +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/output.txt}} ``` 提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用! -当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-18 和 10-19 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。 +当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-17 和 10-18 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。 ### 生命周期注解语法 -生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。 +生命周期注解并不改变任何引用的生命周期的长短。相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。 -生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。 +生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用 `'a` 作为第一个生命周期注解。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。 这里有一些例子:我们有一个没有生命周期参数的 `i32` 的引用,一个有叫做 `'a` 的生命周期参数的 `i32` 的引用,和一个生命周期也是 `'a` 的 `i32` 的可变引用: @@ -101,63 +102,67 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 &'a mut i32 // 带有显式生命周期的可变引用 ``` -单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。 +单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。让我们在 `longest` 函数的上下文中理解生命周期注解如何相互联系。 + + + +例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。 ### 函数签名中的生命周期注解 -现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。 -在这个签名中我们想要表达的限制是所有(两个)参数和返回的引用的生命周期是相关的,也就是这两个参数和返回的引用存活的一样久。就像示例 10-22 中在每个引用中都加上了 `'a` 那样: +为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(*lifetime*)参数,就像泛型类型(*type*)参数一样。 + +我们希望函数签名表达如下限制:也就是这两个参数和返回的引用存活的一样久。(两个)参数和返回的引用的生命周期是相关的。就像示例 10-21 中在每个引用中都加上了 `'a` 那样。 文件名:src/main.rs ```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-22/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/src/main.rs:here}} ``` -示例 10-22:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a` +示例 10-21:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a` -这段代码能够编译并会产生我们希望得到的示例 10-20 中的 `main` 函数的结果。 +这段代码能够编译并会产生我们希望得到的示例 10-19 中的 `main` 函数的结果。 -现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。 +现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。 记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。 当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着 Rust 编译器的工作变得更简单了。如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分。如果不这么做的话,Rust 编译会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离出问题地方很多步之外的代码。 - 当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。 -让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-23 是一个很直观的例子。 +让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-22 是一个很直观的例子。 文件名:src/main.rs ```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-22/src/main.rs:here}} ``` -示例 10-23:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数 +示例 10-22:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数 在这个例子中,`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`。 -接下来,让我们尝试另外一个例子,该例子揭示了 `result` 的引用的生命周期必须是两个参数中较短的那个。以下代码将 `result` 变量的声明移动出内部作用域,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域中。接着,使用了变量 `result` 的 `println!` 也被移动到内部作用域之外。注意示例 10-24 中的代码不能通过编译: +接下来,让我们尝试另外一个例子,该例子揭示了 `result` 的引用的生命周期必须是两个参数中较短的那个。以下代码将 `result` 变量的声明移动出内部作用域,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域中。接着,使用了变量 `result` 的 `println!` 也被移动到内部作用域之外。注意示例 10-23 中的代码不能通过编译: 文件名:src/main.rs ```rust,ignore,does_not_compile -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/src/main.rs:here}} ``` -示例 10-24:尝试在 `string2` 离开作用域之后使用 `result` +示例 10-23:尝试在 `string2` 离开作用域之后使用 `result` 如果尝试编译会出现如下错误: ```console -{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/output.txt}} +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/output.txt}} ``` 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。 -如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。 +如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-23 中的代码,因为它可能会存在无效的引用。 请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确! @@ -171,9 +176,9 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-08-only-one-reference-with-lifetime/src/main.rs:here}} ``` -在这个例子中,我们为参数 `x` 和返回值指定了生命周期参数 `'a`,不过没有为参数 `y` 指定,因为 `y` 的生命周期与参数 `x` 和返回值的生命周期没有任何关系。 +我们为参数 `x` 和返回值指定了生命周期参数 `'a`,不过没有为参数 `y` 指定,因为 `y` 的生命周期与参数 `x` 和返回值的生命周期没有任何关系。 -当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 **没有** 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 `longest` 函数实现: +当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 **没有** 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 `longest` 函数实现: 文件名:src/main.rs @@ -193,31 +198,31 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 ### 结构体定义中的生命周期注解 -目前为止,我们只定义过有所有权类型的结构体。接下来,我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`: +目前为止,我们定义的结构体全都包含拥有所有权的类型。也可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例 10-24 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`。 文件名:src/main.rs ```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-25/src/main.rs}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/src/main.rs}} ``` -示例 10-25:一个存放引用的结构体,所以其定义需要生命周期注解 +示例 10-24:一个存放引用的结构体,所以其定义需要生命周期注解 -这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。 +这个结构体有唯一一个字段 `part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。 这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。`novel` 的数据在 `ImportantExcerpt` 实例创建之前就存在。另外,直到 `ImportantExcerpt` 离开作用域之后 `novel` 都不会离开作用域,所以 `ImportantExcerpt` 实例中的引用是有效的。 ### 生命周期省略(Lifetime Elision) -现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期注解却能编译成功: +现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,如示例 10-25 所示,它没有生命周期注解却能编译成功: 文件名:src/lib.rs ```rust -{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-26/src/main.rs:here}} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-25/src/main.rs:here}} ``` -示例 10-26:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 +示例 10-25:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: @@ -231,19 +236,19 @@ fn first_word<'a>(s: &'a str) -> &'a str { 被编码进 Rust 引用分析的模式被称为 **生命周期省略规则**(*lifetime elision rules*)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。 -省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。 +省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。不用于猜测,编译器在可以通过增加生命周期注解解决的地方会给出一个错误。 函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。 编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 `fn` 定义,以及 `impl` 块。 -第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 +第一条规则是编译器为每一个是引用参数都分配了一个生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 `&self` 或 `&mut self`,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 `self` 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。 -假设我们自己就是编译器。并应用这些规则来计算示例 10-26 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: +假设我们自己就是编译器。并应用这些规则来计算示例 10-25 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: ```rust,ignore fn first_word(s: &str) -> &str { @@ -263,7 +268,7 @@ fn first_word<'a>(s: &'a str) -> &'a str { 现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。 -让我们再看看另一个例子,这次我们从示例 10-21 中没有生命周期参数的 `longest` 函数开始: +让我们再看看另一个例子,这次我们从示例 10-20 中没有生命周期参数的 `longest` 函数开始: ```rust,ignore fn longest(x: &str, y: &str) -> &str { @@ -275,7 +280,7 @@ fn longest(x: &str, y: &str) -> &str { fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { ``` -再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 `self` 参数。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是为什么在编译示例 10-21 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。 +再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 `self` 参数。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是为什么在编译示例 10-20 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。 因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。 @@ -285,7 +290,7 @@ fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { (实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。 -`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-25 中定义的结构体 `ImportantExcerpt` 的例子。 +`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-24 中定义的结构体 `ImportantExcerpt` 的例子。 首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值: @@ -313,7 +318,7 @@ let s: &'static str = "I have a static lifetime."; 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 `'static` 的。 -你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你也许要考虑是否希望它存在得这么久,即使这是可能的。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。 +你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效,以及你否希望它存在得这么久。大部分情况,推荐 `'static` 生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个 `'static` 的生命周期。 ### 结合泛型类型参数、trait bounds 和生命周期 @@ -323,7 +328,7 @@ let s: &'static str = "I have a static lifetime."; {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-11-generics-traits-and-lifetimes/src/main.rs:here}} ``` -这个是示例 10-22 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会使用 `{}` 打印,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 +这个是示例 10-21 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会使用 `{}` 打印,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 ## 总结