diff --git a/src/ch10-00-generics.md b/src/ch10-00-generics.md index f920d5a..44bb4c4 100644 --- a/src/ch10-00-generics.md +++ b/src/ch10-00-generics.md @@ -2,7 +2,7 @@ > [ch10-00-generics.md](https://github.com/rust-lang/book/blob/main/src/ch10-00-generics.md) >
-> commit 48b057106646758f6453f42b7887f34b8c24caf6 +> commit 9c0fa2714859738ff73cbbb829592e4c037d7e46 每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。 @@ -23,20 +23,7 @@ 文件名: src/main.rs ```rust -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let mut largest = number_list[0]; - - for number in number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {}", largest); -# assert_eq!(largest, 100); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-01/src/main.rs:here}} ``` 示例 10-1:在一个数字列表中寻找最大值的函数 @@ -48,31 +35,7 @@ fn main() { 文件名: src/main.rs ```rust -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let mut largest = number_list[0]; - - for number in number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {}", largest); - - let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; - - let mut largest = number_list[0]; - - for number in number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {}", largest); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-02/src/main.rs}} ``` 示例 10-2:寻找 **两个** 数字列表最大值的代码 @@ -86,36 +49,12 @@ fn main() { 文件名: src/main.rs ```rust -fn largest(list: &[i32]) -> i32 { - let mut largest = list[0]; - - for &item in list { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest(&number_list); - println!("The largest number is {}", result); -# assert_eq!(result, 100); - - let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; - - let result = largest(&number_list); - println!("The largest number is {}", result); -# assert_eq!(result, 6000); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-03/src/main.rs:here}} ``` 示例 10-3:抽象后的寻找两个数字列表最大值的代码 -`largest` 函数有一个参数 `list`,它代表会传递给函数的任何具体的 `i32`值的 slice。函数定义中的 `list` 代表任何 `&[i32]`。当调用 `largest` 函数时,其代码实际上运行于我们传递的特定值上。 +`largest` 函数有一个参数 `list`,它代表会传递给函数的任何具体的 `i32`值的 slice。函数定义中的 `list` 代表任何 `&[i32]`。当调用 `largest` 函数时,其代码实际上运行于我们传递的特定值上。目前不需要担心 `for` 循环的语法。这里不是引用了一个 `i32` 的引用,这里只是模式匹配并表明循环的值应该是 `&i32`。[第十八章][ch18] 会详细介绍模式匹配。 总的来说,从示例 10-2 到示例 10-3 中涉及的机制经历了如下几步: @@ -126,3 +65,5 @@ fn main() { 在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。与函数体可以在抽象`list`而不是特定值上操作的方式相同,泛型允许代码对抽象类型进行操作。 如果我们有两个函数,一个寻找一个 `i32` 值的 slice 中的最大项而另一个寻找 `char` 值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待! + +[ch18]: ch18-00-patterns.html diff --git a/src/ch10-01-syntax.md b/src/ch10-01-syntax.md index cce10a0..b3b40df 100644 --- a/src/ch10-01-syntax.md +++ b/src/ch10-01-syntax.md @@ -2,7 +2,7 @@ > [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/main/src/ch10-01-syntax.md) >
-> commit af34ac954a6bd7fc4a8bbcc5c9685e23c5af87da +> commit 6a23b2c7ffe392d1329855444a1b3a88e93982b6 我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。 @@ -15,43 +15,7 @@ 文件名: src/main.rs ```rust -fn largest_i32(list: &[i32]) -> i32 { - let mut largest = list[0]; - - for &item in list.iter() { - if item > largest { - largest = item; - } - } - - largest -} - -fn largest_char(list: &[char]) -> char { - let mut largest = list[0]; - - for &item in list.iter() { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest_i32(&number_list); - println!("The largest number is {}", result); -# assert_eq!(result, 100); - - let char_list = vec!['y', 'm', 'a', 'q']; - - let result = largest_char(&char_list); - println!("The largest char is {}", result); -# assert_eq!(result, 'y'); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-04/src/main.rs:here}} ``` 示例 10-4:两个函数,不同点只是名称和签名类型 @@ -73,43 +37,15 @@ fn largest(list: &[T]) -> T { 文件名: src/main.rs ```rust,ignore,does_not_compile -fn largest(list: &[T]) -> T { - let mut largest = list[0]; - - for &item in list.iter() { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest(&number_list); - println!("The largest number is {}", result); - - let char_list = vec!['y', 'm', 'a', 'q']; - - let result = largest(&char_list); - println!("The largest char is {}", result); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/src/main.rs}} ``` 示例 10-5:一个使用泛型参数的 `largest` 函数定义,尚不能编译 如果现在就编译这个代码,会出现如下错误: -```text -error[E0369]: binary operation `>` cannot be applied to type `T` - --> src/main.rs:5:12 - | -5 | if item > largest { - | ^^^^^^^^^^^^^^ - | - = note: an implementation of `std::cmp::PartialOrd` might be missing for `T` +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/output.txt}} ``` 注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait。不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型。因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)。 @@ -123,15 +59,7 @@ error[E0369]: binary operation `>` cannot be applied to type `T` 文件名: src/main.rs ```rust -struct Point { - x: T, - y: T, -} - -fn main() { - let integer = Point { x: 5, y: 10 }; - let float = Point { x: 1.0, y: 4.0 }; -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-06/src/main.rs}} ``` 示例 10-6:`Point` 结构体存放了两个 `T` 类型的值 `x` 和 `y` @@ -143,30 +71,15 @@ fn main() { 文件名: src/main.rs ```rust,ignore,does_not_compile -struct Point { - x: T, - y: T, -} - -fn main() { - let wont_work = Point { x: 5, y: 4.0 }; -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/src/main.rs}} ``` 示例 10-7:字段 `x` 和 `y` 的类型必须相同,因为他们都有相同的泛型类型 `T` 在这个例子中,当把整型值 5 赋值给 `x` 时,就告诉了编译器这个 `Point` 实例中的泛型 `T` 是整型的。接着指定 `y` 为 4.0,它被定义为与 `x` 相同类型,就会得到一个像这样的类型不匹配错误: -```text -error[E0308]: mismatched types - --> src/main.rs:7:38 - | -7 | let wont_work = Point { x: 5, y: 4.0 }; - | ^^^ expected integer, found -floating-point number - | - = note: expected type `{integer}` - found type `{float}` +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/output.txt}} ``` 如果想要定义一个 `x` 和 `y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在示例 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T` 和 `U`。其中字段 `x` 是 `T` 类型的,而字段 `y` 是 `U` 类型的: @@ -174,16 +87,7 @@ floating-point number 文件名: src/main.rs ```rust -struct Point { - x: T, - y: U, -} - -fn main() { - let both_integer = Point { x: 5, y: 10 }; - let both_float = Point { x: 1.0, y: 4.0 }; - let integer_and_float = Point { x: 5, y: 4.0 }; -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-08/src/main.rs}} ``` 示例 10-8:使用两个泛型的 `Point`,这样 `x` 和 `y` 可能是不同类型 @@ -223,83 +127,38 @@ enum Result { 文件名: src/main.rs ```rust -struct Point { - x: T, - y: T, -} - -impl Point { - fn x(&self) -> &T { - &self.x - } -} - -fn main() { - let p = Point { x: 5, y: 10 }; - - println!("p.x = {}", p.x()); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-09/src/main.rs}} ``` 示例 10-9:在 `Point` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用 这里在 `Point` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用: -注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。 +注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。因为再次声明了泛型,我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。`impl` 中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。 -例如,可以选择为 `Point` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`: +另一个选择是定义方法适用于某些有限制(constraint)的泛型类型。例如,可以选择为 `Point` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`: ```rust -# struct Point { -# x: T, -# y: T, -# } -# -impl Point { - fn distance_from_origin(&self) -> f32 { - (self.x.powi(2) + self.y.powi(2)).sqrt() - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-10/src/main.rs:here}} ``` 示例 10-10:构建一个只用于拥有泛型参数 `T` 的结构体的具体类型的 `impl` 块 这段代码意味着 `Point` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point` 实例则没有定义此方法。这个方法计算点实例与坐标 (0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。 -结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中在示例 10-8 中的结构体 `Point` 上定义了一个方法 `mixup`。这个方法获取另一个 `Point` 作为参数,而它可能与调用 `mixup` 的 `self` 是不同的 `Point` 类型。这个方法用 `self` 的 `Point` 类型的 `x` 值(类型 `T`)和参数的 `Point` 类型的 `y` 值(类型 `W`)来创建一个新 `Point` 类型的实例: +结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中为 `Point` 结构体使用了泛型类型 `X1` 和 `Y1`,为 `mixup` 方法签名使用了 `X2` 和 `Y2` 来使得示例更加清楚。这个方法用 `self` 的 `Point` 类型的 `x` 值(类型 `X1`)和参数的 `Point` 类型的 `y` 值(类型 `Y2`)来创建一个新 `Point` 类型的实例: 文件名: src/main.rs ```rust -struct Point { - x: T, - y: U, -} - -impl Point { - fn mixup(self, other: Point) -> Point { - Point { - x: self.x, - y: other.y, - } - } -} - -fn main() { - let p1 = Point { x: 5, y: 10.4 }; - let p2 = Point { x: "Hello", y: 'c'}; - - let p3 = p1.mixup(p2); - - println!("p3.x = {}, p3.y = {}", p3.x, p3.y); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-11/src/main.rs}} ``` 示例 10-11:方法使用了与结构体定义中不同类型的泛型 在 `main` 函数中,定义了一个有 `i32` 类型的 `x`(其值为 `5`)和 `f64` 的 `y`(其值为 `10.4`)的 `Point`。`p2` 则是一个有着字符串 slice 类型的 `x`(其值为 `"Hello"`)和 `char` 类型的 `y`(其值为`c`)的 `Point`。在 `p1` 上以 `p2` 作为参数调用 `mixup` 会返回一个 `p3`,它会有一个 `i32` 类型的 `x`,因为 `x` 来自 `p1`,并拥有一个 `char` 类型的 `y`,因为 `y` 来自 `p2`。`println!` 会打印出 `p3.x = 5, p3.y = c`。 -这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `T` 和 `U` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `V` 和 `W` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。 +这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `X1` 和 `Y1` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `X2` 和 `Y2` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。 ### 泛型代码的性能 @@ -341,4 +200,4 @@ fn main() { 我们可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。 -[traits-as-parameters]: ch10-02-traits.html#traits-as-parameters +[traits-as-parameters]: ch10-02-traits.html#trait-作为参数 diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index 3cfba04..3daa495 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 34b403864ad9c5e27b00b7cc4a6893804ef5b989 +> commit 3c2ca8528c3b92b7d30e73f2e8a1b84b2f68b0c8 *trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。 @@ -14,19 +14,17 @@ 例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。 -我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summarize` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summary` trait 的定义: +我们想要创建一个名为 `aggregator` 的多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summarize` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的公有 `Summary` trait 的定义: 文件名: src/lib.rs -```rust -pub trait Summary { - fn summarize(&self) -> String; -} +```rust,noplayground +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-12/src/lib.rs}} ``` 示例 10-12:`Summary` trait 定义,它包含由 `summarize` 方法提供的行为 -这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。 +这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。我们也声明 crate 为 `pub` 以便依赖这个 crate 的也可以使用这个 crate,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。 在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summary` trait 的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。 @@ -34,64 +32,27 @@ trait 体中可以有多个方法:一行一个方法签名且都以分号结 ### 为类型实现 trait -现在我们定义了 `Summary` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。 +现在我们定义了 `Summary` trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。 文件名: src/lib.rs -```rust -# pub trait Summary { -# fn summarize(&self) -> String; -# } -# -pub struct NewsArticle { - pub headline: String, - pub location: String, - pub author: String, - pub content: String, -} - -impl Summary for NewsArticle { - fn summarize(&self) -> String { - format!("{}, by {} ({})", self.headline, self.author, self.location) - } -} - -pub struct Tweet { - pub username: String, - pub content: String, - pub reply: bool, - pub retweet: bool, -} - -impl Summary for Tweet { - fn summarize(&self) -> String { - format!("{}: {}", self.username, self.content) - } -} +```rust,noplayground +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-13/src/lib.rs:here}} ``` 示例 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summary` trait 在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。 -一旦实现了 trait,我们就可以用与 `NewsArticle` 和 `Tweet` 实例的非 trait 方法一样的方式调用 trait 方法了: +现在库在 `NewsArticle` 和 `Tweet` 上实现了`Summary` trait,crate 的用户可以像调用常规方法一样调用 `NewsArticle` 和 `Tweet` 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 `aggregator` 库 crate 的例子: ```rust,ignore -let tweet = Tweet { - username: String::from("horse_ebooks"), - content: String::from("of course, as you probably already know, people"), - reply: false, - retweet: false, -}; - -println!("1 new tweet: {}", tweet.summarize()); +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-01-calling-trait-method/src/main.rs}} ``` 这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people`。 -注意因为示例 10-13 中我们在相同的 *lib.rs* 里定义了 `Summary` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 *lib.rs* 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 `Summary` trait。首先他们需要将 trait 引入作用域。这可以通过指定 `use aggregator::Summary;` 实现,这样就可以为其类型实现 `Summary` trait 了。`Summary` 还必须是公有 trait 使得其他 crate 可以实现它,这也是为什么实例 10-12 中将 `pub` 置于 `trait` 之前。 - -实现 trait 时需要注意的一个限制是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 `aggregator` crate 的自定义类型 `Tweet` 实现如标准库中的 `Display` trait,这是因为 `Tweet` 类型位于 `aggregator` crate 本地的作用域中。类似地,也可以在 `aggregator` crate 中为 `Vec` 实现 `Summary`,这是因为 `Summary` trait 位于 `aggregator` crate 本地作用域中。 +其他依赖 `aggregator` crate 的 crate 也可以将 `Summary` 引入作用域以便为其自己的类型实现该 trait。实现 trait 时需要注意的一个限制是,只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 `aggregator` crate 的自定义类型 `Tweet` 实现如标准库中的 `Display` trait,这是因为 `Tweet` 类型位于 `aggregator` crate 本地的作用域中。类似地,也可以在 `aggregator` crate 中为 `Vec` 实现 `Summary`,这是因为 `Summary` trait 位于 `aggregator` crate 本地作用域中。 但是不能为外部类型实现外部 trait。例如,不能在 `aggregator` crate 中为 `Vec` 实现 `Display` trait。这是因为 `Display` 和 `Vec` 都定义于标准库中,它们并不位于 `aggregator` crate 本地作用域中。这个限制是被称为 **相干性**(*coherence*) 的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。 @@ -103,12 +64,8 @@ println!("1 new tweet: {}", tweet.summarize()); 文件名: src/lib.rs -```rust -pub trait Summary { - fn summarize(&self) -> String { - String::from("(Read more...)") - } -} +```rust,noplayground +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-14/src/lib.rs:here}} ``` 示例 10-14:`Summary` trait 的定义,带有一个 `summarize` 方法的默认实现 @@ -118,15 +75,7 @@ pub trait Summary { 虽然我们不再直接为 `NewsArticle` 定义 `summarize` 方法了,但是我们提供了一个默认实现并且指定 `NewsArticle` 实现 `Summary` trait。因此,我们仍然可以对 `NewsArticle` 实例调用 `summarize` 方法,如下所示: ```rust,ignore -let article = NewsArticle { - headline: String::from("Penguins win the Stanley Cup Championship!"), - location: String::from("Pittsburgh, PA, USA"), - author: String::from("Iceburgh"), - content: String::from("The Pittsburgh Penguins once again are the best - hockey team in the NHL."), -}; - -println!("New article available! {}", article.summarize()); +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-02-calling-default-impl/src/main.rs:here}} ``` 这段代码会打印 `New article available! (Read more...)`。 @@ -135,37 +84,20 @@ println!("New article available! {}", article.summarize()); 默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 `Summary` trait,使其具有一个需要实现的 `summarize_author` 方法,然后定义一个 `summarize` 方法,此方法的默认实现调用 `summarize_author` 方法: -```rust -pub trait Summary { - fn summarize_author(&self) -> String; - - fn summarize(&self) -> String { - format!("(Read more from {}...)", self.summarize_author()) - } -} +```rust,noplayground +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:here}} ``` 为了使用这个版本的 `Summary`,只需在实现 trait 时定义 `summarize_author` 即可: ```rust,ignore -impl Summary for Tweet { - fn summarize_author(&self) -> String { - format!("@{}", self.username) - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:impl}} ``` 一旦定义了 `summarize_author`,我们就可以对 `Tweet` 结构体的实例调用 `summarize` 了,而 `summarize` 的默认实现会调用我们提供的 `summarize_author` 定义。因为实现了 `summarize_author`,`Summary` trait 就提供了 `summarize` 方法的功能,且无需编写更多的代码。 ```rust,ignore -let tweet = Tweet { - username: String::from("horse_ebooks"), - content: String::from("of course, as you probably already know, people"), - reply: false, - retweet: false, -}; - -println!("1 new tweet: {}", tweet.summarize()); +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/main.rs:here}} ``` 这会打印出 `1 new tweet: (Read more from @horse_ebooks...)`。 @@ -179,9 +111,7 @@ println!("1 new tweet: {}", tweet.summarize()); 例如在示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summary` trait。我们可以定义一个函数 `notify` 来调用其参数 `item` 上的 `summarize` 方法,该参数是实现了 `Summary` trait 的某种类型。为此可以使用 `impl Trait` 语法,像这样: ```rust,ignore -pub fn notify(item: impl Summary) { - println!("Breaking news! {}", item.summarize()); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-04-traits-as-parameters/src/lib.rs:here}} ``` 对于 `item` 参数,我们指定了 `impl` 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 `notify` 函数体中,可以调用任何来自 `Summary` trait 的方法,比如 `summarize`。我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例来调用 `notify`。任何用其它如 `String` 或 `i32` 的类型调用该函数的代码都不能编译,因为它们没有实现 `Summary`。 @@ -191,7 +121,7 @@ pub fn notify(item: impl Summary) { `impl Trait` 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 *trait bound*,它看起来像: ```rust,ignore -pub fn notify(item: T) { +pub fn notify(item: &T) { println!("Breaking news! {}", item.summarize()); } ``` @@ -201,13 +131,13 @@ pub fn notify(item: T) { `impl Trait` 很方便,适用于短小的例子。trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 `Summary` 的参数。使用 `impl Trait` 的语法看起来像这样: ```rust,ignore -pub fn notify(item1: impl Summary, item2: impl Summary) { +pub fn notify(item1: &impl Summary, item2: &impl Summary) { ``` 这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能: ```rust,ignore -pub fn notify(item1: T, item2: T) { +pub fn notify(item1: &T, item2: &T) { ``` 泛型 `T` 被指定为 `item1` 和 `item2` 的参数限制,如此传递给参数 `item1` 和 `item2` 值的具体类型必须一致。 @@ -217,13 +147,13 @@ pub fn notify(item1: T, item2: T) { 如果 `notify` 需要显示 `item` 的格式化形式,同时也要使用 `summarize` 方法,那么 `item` 就需要同时实现两个不同的 trait:`Display` 和 `Summary`。这可以通过 `+` 语法实现: ```rust,ignore -pub fn notify(item: impl Summary + Display) { +pub fn notify(item: &(impl Summary + Display)) { ``` `+` 语法也适用于泛型的 trait bound: ```rust,ignore -pub fn notify(item: T) { +pub fn notify(item: &T) { ``` 通过指定这两个 trait bound,`notify` 的函数体可以调用 `summarize` 并使用 `{}` 来格式化 `item`。 @@ -233,13 +163,13 @@ pub fn notify(item: T) { 然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 `where` 从句中指定 trait bound 的语法。所以除了这么写: ```rust,ignore -fn some_function(t: T, u: U) -> i32 { +fn some_function(t: &T, u: &U) -> i32 { ``` 还可以像这样使用 `where` 从句: ```rust,ignore -fn some_function(t: T, u: U) -> i32 +fn some_function(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug { @@ -252,14 +182,7 @@ fn some_function(t: T, u: U) -> i32 也可以在返回值中使用 `impl Trait` 语法,来返回实现了某个 trait 的类型: ```rust,ignore -fn returns_summarizable() -> impl Summary { - Tweet { - username: String::from("horse_ebooks"), - content: String::from("of course, as you probably already know, people"), - reply: false, - retweet: false, - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-05-returning-impl-trait/src/lib.rs:here}} ``` 通过使用 `impl Summary` 作为返回值类型,我们指定了 `returns_summarizable` 函数返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型。在这个例子中 `returns_summarizable` 返回了一个 `Tweet`,不过调用方并不知情。 @@ -269,24 +192,7 @@ fn returns_summarizable() -> impl Summary { 不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 `impl Summary`,但是返回了 `NewsArticle` 或 `Tweet` 就行不通: ```rust,ignore,does_not_compile -fn returns_summarizable(switch: bool) -> impl Summary { - if switch { - NewsArticle { - headline: String::from("Penguins win the Stanley Cup Championship!"), - location: String::from("Pittsburgh, PA, USA"), - author: String::from("Iceburgh"), - content: String::from("The Pittsburgh Penguins once again are the best - hockey team in the NHL."), - } - } else { - Tweet { - username: String::from("horse_ebooks"), - content: String::from("of course, as you probably already know, people"), - reply: false, - retweet: false, - } - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-06-impl-trait-returns-one-type/src/lib.rs:here}} ``` 这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。第十七章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。 @@ -295,42 +201,20 @@ fn returns_summarizable(switch: bool) -> impl Summary { 现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!回顾一下,最后尝试编译代码时出现的错误是: -```text -error[E0369]: binary operation `>` cannot be applied to type `T` - --> src/main.rs:5:12 - | -5 | if item > largest { - | ^^^^^^^^^^^^^^ - | - = note: an implementation of `std::cmp::PartialOrd` might be missing for `T` +```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 -fn largest(list: &[T]) -> T { +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-fixing-listing-10-05/src/main.rs:here}} ``` 但是如果编译代码的话,会出现一些不同的错误: -```text -error[E0508]: cannot move out of type `[T]`, a non-copy slice - --> src/main.rs:2:23 - | -2 | let mut largest = list[0]; - | ^^^^^^^ - | | - | cannot move out of here - | help: consider using a reference instead: `&list[0]` - -error[E0507]: cannot move out of borrowed content - --> src/main.rs:4:9 - | -4 | for &item in list.iter() { - | ^---- - | || - | |hint: to prevent move, use `ref item` or `ref mut item` - | cannot move out of borrowed content +```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` 变量中,这导致了上面的错误。 @@ -340,67 +224,21 @@ error[E0507]: cannot move out of borrowed content 文件名: src/main.rs ```rust -fn largest(list: &[T]) -> T { - let mut largest = list[0]; - - for &item in list.iter() { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest(&number_list); - println!("The largest number is {}", result); - - let char_list = vec!['y', 'm', 'a', 'q']; - - let result = largest(&char_list); - println!("The largest char is {}", result); -} +{{#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 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! +另一种 `largest` 的实现方式是返回在 slice 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!如果你无法摆脱与生命周期有关的错误,请继续阅读:接下来的 “生命周期与引用有效性” 部分会详细的说明,不过声明周期对于解决这些挑战来说并不是必须的。 ### 使用 trait bound 有条件地实现方法 -通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法,不过只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair` 才会实现 `cmp_display` 方法: +通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法并返回一个 `Pair` 的实例(回忆一下第五章的 ["定义方法"][methods] 部分,`Self` 是一个 `impl` 块类型的类型别名(type alias),在这里是 `Pair`)。不过在下一个 `impl` 块中,只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair` 才会实现 `cmp_display` 方法: -```rust -use std::fmt::Display; - -struct Pair { - x: T, - y: T, -} - -impl Pair { - fn new(x: T, y: T) -> Self { - Self { - x, - y, - } - } -} - -impl Pair { - fn cmp_display(&self) { - if self.x >= self.y { - println!("The largest member is x = {}", self.x); - } else { - println!("The largest member is y = {}", self.y); - } - } -} +```rust,noplayground +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/src/lib.rs}} ``` 示例 10-16:根据 trait bound 在泛型上有条件的实现方法 @@ -426,6 +264,7 @@ trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍 这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是 **生命周期**(*lifetimes*)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。 [stack-only-data-copy]: -ch04-01-what-is-ownership.html#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#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 901b7be..8c1313c 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -2,27 +2,18 @@ > [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/main/src/ch10-03-lifetime-syntax.md) >
-> commit 426f3e4ec17e539ae9905ba559411169d303a031 +> commit 1b8746013079f2e2ce1c8e85f633d9769778ea7f 当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 -生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。 +生命周期注解甚至不是一个大部分语言都有的概念,所以这可能感觉起来有些陌生。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。 ### 生命周期避免了悬垂引用 生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-17 中的程序,它有一个外部作用域和一个内部作用域. ```rust,ignore,does_not_compile -{ - let r; - - { - let x = 5; - r = &x; - } - - println!("r: {}", r); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/src/main.rs:here}} ``` 示例 10-17:尝试使用离开作用域的值的引用 @@ -31,17 +22,8 @@ 外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值。这段代码不能编译因为 `r` 引用的值在尝试使用之前就离开了作用域。如下是错误信息: -```text -error[E0597]: `x` does not live long enough - --> src/main.rs:7:5 - | -6 | r = &x; - | - borrow occurs here -7 | } - | ^ `x` dropped here while still borrowed -... -10 | } - | - borrowed value needs to live until here +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/output.txt}} ``` 变量 `x` 并没有 “存在的足够久”。其原因是 `x` 在到达第 7 行内部作用域结束时就离开了作用域。不过 `r` 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?这得益于借用检查器。 @@ -51,16 +33,7 @@ error[E0597]: `x` does not live long enough Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-18 展示了与示例 10-17 相同的例子不过带有变量生命周期的注释: ```rust,ignore,does_not_compile -{ - let r; // ---------+-- 'a - // | - { // | - let x = 5; // -+-- 'b | - r = &x; // | | - } // -+ | - // | - println!("r: {}", r); // | -} // ---------+ +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-18/src/main.rs:here}} ``` 示例 10-18:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b` @@ -70,16 +43,10 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 让我们看看示例 10-19 中这个并没有产生悬垂引用且可以正确编译的例子: ```rust -{ - let x = 5; // ----------+-- 'b - // | - let r = &x; // --+-- 'a | - // | | - println!("r: {}", r); // | | - // --+ | -} // ----------+ +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-19/src/main.rs:here}} ``` + 示例 10-19:一个有效的引用,因为数据比引用有着更长的生命周期 这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`:Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。 @@ -93,48 +60,27 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作 文件名: src/main.rs ```rust,ignore -fn main() { - let string1 = String::from("abcd"); - let string2 = "xyz"; - - let result = longest(string1.as_str(), string2); - println!("The longest string is {}", result); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/src/main.rs}} ``` 示例 10-20:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个 -注意这个函数获取作为引用的字符串 slice,因为我们不希望 `longest` 函数获取参数的所有权。我们期望该函数接受 `String` 的 slice(参数 `string1` 的类型)和字符串字面值(包含于参数 `string2`) - -参考之前第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。 +注意这个函数获取作为引用的字符串 slice,因为我们不希望 `longest` 函数获取参数的所有权。参考之前第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。 如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译: 文件名: src/main.rs ```rust,ignore,does_not_compile -fn longest(x: &str, y: &str) -> &str { - if x.len() > y.len() { - x - } else { - y - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/src/main.rs:here}} ``` 示例 10-21:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译 相应地会出现如下有关生命周期的错误: -```text -error[E0106]: missing lifetime specifier - --> src/main.rs:1:33 - | -1 | fn longest(x: &str, y: &str) -> &str { - | ^ expected lifetime parameter - | - = help: this function's return type contains a borrowed value, but the -signature does not say whether it is borrowed from `x` or `y` +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/output.txt}} ``` 提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用! @@ -159,27 +105,25 @@ signature does not say whether it is borrowed from `x` or `y` ### 函数签名中的生命周期注解 -现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-22 中在每个引用中都加上了 `'a` 那样: +现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。 +在这个签名中我们想要表达的限制是所有(两个)参数和返回的引用的生命周期是相关的,也就是这两个参数和返回的引用存活的一样久。就像示例 10-22 中在每个引用中都加上了 `'a` 那样: 文件名: src/main.rs ```rust -fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { - if x.len() > y.len() { - x - } else { - y - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-22/src/main.rs:here}} ``` 示例 10-22:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a` 这段代码能够编译并会产生我们希望得到的示例 10-20 中的 `main` 函数的结果。 -现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这就是我们告诉 Rust 需要其保证的约束条件。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。 +现在函数签名表明对于某些生命周期 `'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` 中较短的那个生命周期结束之前保持有效。 @@ -188,23 +132,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 文件名: src/main.rs ```rust -# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { -# if x.len() > y.len() { -# x -# } else { -# y -# } -# } -# -fn main() { - let string1 = String::from("long string is long"); - - { - let string2 = String::from("xyz"); - let result = longest(string1.as_str(), string2.as_str()); - println!("The longest string is {}", result); - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/src/main.rs:here}} ``` 示例 10-23:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数 @@ -216,31 +144,15 @@ fn main() { 文件名: src/main.rs ```rust,ignore,does_not_compile -fn main() { - let string1 = String::from("long string is long"); - let result; - { - let string2 = String::from("xyz"); - result = longest(string1.as_str(), string2.as_str()); - } - println!("The longest string is {}", result); -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/src/main.rs:here}} ``` 示例 10-24:尝试在 `string2` 离开作用域之后使用 `result` 如果尝试编译会出现如下错误: -```text -error[E0597]: `string2` does not live long enough - --> src/main.rs:6:44 - | -6 | result = longest(string1.as_str(), string2.as_str()); - | ^^^^^^^ borrowed value does not live long enough -7 | } - | - `string2` dropped here while still borrowed -8 | println!("The longest string is {}", result); - | ------ borrow later used here +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/output.txt}} ``` 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。 @@ -256,9 +168,7 @@ error[E0597]: `string2` does not live long enough 文件名: src/main.rs ```rust -fn longest<'a>(x: &'a str, y: &str) -> &'a str { - x -} +{{#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` 和返回值的生命周期没有任何关系。 @@ -268,32 +178,13 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str { 文件名: src/main.rs ```rust,ignore,does_not_compile -fn longest<'a>(x: &str, y: &str) -> &'a str { - let result = String::from("really long string"); - result.as_str() -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-09-unrelated-lifetime/src/main.rs:here}} ``` 即便我们为返回值指定了生命周期参数 `'a`,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息: -```text -error[E0597]: `result` does not live long enough - --> src/main.rs:3:5 - | -3 | result.as_str() - | ^^^^^^ does not live long enough -4 | } - | - borrowed value only lives until here - | -note: borrowed value must be valid for the lifetime 'a as defined on the -function body at 1:1... - --> src/main.rs:1:1 - | -1 | / fn longest<'a>(x: &str, y: &str) -> &'a str { -2 | | let result = String::from("really long string"); -3 | | result.as_str() -4 | | } - | |_^ +```console +{{#include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-09-unrelated-lifetime/output.txt}} ``` 出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。 @@ -307,17 +198,7 @@ function body at 1:1... 文件名: src/main.rs ```rust -struct ImportantExcerpt<'a> { - part: &'a str, -} - -fn main() { - let novel = String::from("Call me Ishmael. Some years ago..."); - let first_sentence = novel.split('.') - .next() - .expect("Could not find a '.'"); - let i = ImportantExcerpt { part: first_sentence }; -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-25/src/main.rs}} ``` 示例 10-25:一个存放引用的结构体,所以其定义需要生命周期注解 @@ -333,17 +214,7 @@ fn main() { 文件名: src/lib.rs ```rust -fn first_word(s: &str) -> &str { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return &s[0..i]; - } - } - - &s[..] -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-26/src/main.rs:here}} ``` 示例 10-26:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 @@ -419,15 +290,7 @@ fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { 首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值: ```rust -# struct ImportantExcerpt<'a> { -# part: &'a str, -# } -# -impl<'a> ImportantExcerpt<'a> { - fn level(&self) -> i32 { - 3 - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:1st}} ``` `impl` 之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 `self` 引用的生命周期。 @@ -435,16 +298,7 @@ impl<'a> ImportantExcerpt<'a> { 这里是一个适用于第三条生命周期省略规则的例子: ```rust -# struct ImportantExcerpt<'a> { -# part: &'a str, -# } -# -impl<'a> ImportantExcerpt<'a> { - fn announce_and_return_part(&self, announcement: &str) -> &str { - println!("Attention please: {}", announcement); - self.part - } -} +{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:3rd}} ``` 这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 `&self` 和 `announcement` 他们各自的生命周期。接着,因为其中一个参数是 `&self`,返回值类型被赋予了 `&self` 的生命周期,这样所有的生命周期都被计算出来了。 @@ -466,29 +320,19 @@ let s: &'static str = "I have a static lifetime."; 让我们简要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法! ```rust -use std::fmt::Display; - -fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str - where T: Display -{ - println!("Announcement! {}", ann); - if x.len() > y.len() { - x - } else { - y - } -} +{{#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 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 +这个是示例 10-22 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会使用 `{}` 打印,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 ## 总结 这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型,你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率! -你可能不会相信,这个话题还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! +你可能不会相信,这个话题还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。还有更多更复杂的涉及生命周期注解的场景,只有在非常高级的情况下才会需要它们;对于这些内容,请阅读 [Rust Reference][reference]。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! [references-and-borrowing]: -ch04-02-references-and-borrowing.html#references-and-borrowing +ch04-02-references-and-borrowing.html#引用与借用 [string-slices-as-parameters]: -ch04-03-slices.html#string-slices-as-parameters +ch04-03-slices.html#字符串-slice-作为参数 +[reference]: https://doc.rust-lang.org/reference/index.html