泛型、Trait和生命周期

本文大部分内容翻译自:The Rust Programming Language

1. 泛型数据类型

1.1. 在函数定义中使用泛型

当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。
例:原本求i32charlargest函数,不同之处只有名称和签名类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];

for &item in list {
if item > largest {
largest = item;
}
}

largest
}

fn largest_char(list: &[char]) -> char {
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_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');
}

定义泛型:

  • 函数声明:fn largest<T>(list: &[T]) -> T {
    • 泛型参数:T,是“type”的缩写
    • 尖括号<>:类型参数声明位于函数名称与参数列表中间的尖括号中
  • 可以这样理解这个定义:函数largest有泛型类型T。它有个参数list,其类型是元素为T的slice。largest函数的返回值类型也是T

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn largest<T>(list: &[T]) -> T {
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);

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest(&char_list);
println!("The largest char is {}", result);
}

目前会产生编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

这个错误表明largest的函数体不能适用于T的所有可能的类型,因为在函数体需要比较T类型的值,它只能用于我们知道如何排序的类型。标准库中定义的std::cmp::PartialOrd trait可以实现类型的比较功能。

标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能。

1.2. 结构体定义中的泛型

  • 必须在结构体名称后面的尖括号中声明泛型参数的名称
  • 在结构体定义中可以指定具体数据类型的位置使用泛型类型

例:

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

上面定义中,xy是相同类型的,如果创建了不同类型的xy则不能编译。

如果想定义不同类型的xy,如下:

1
2
3
4
5
6
7
8
9
10
struct Point<T, U> {
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 };
}

1.3. 枚举定义中的泛型

例如Option<T>

1
2
3
4
enum Option<T> {
Some(T),
None,
}

枚举也可以拥有多个泛型类型,例如Result枚举:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

1.4. 方法定义中的泛型

1.4.1. 方法定义中的泛型

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

fn main() {
let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());
}
  • 必须在impl后面声明T,这样就可以在Point<T>上实现的方法中使用它了。
    • impl之后声明泛型T,这样Rust就知道Point的尖括号中的类型是泛型而不是具体类型。
    • 因为再次声明了泛型,我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。
  • impl中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。

1.4.2. 有限制(constraint)的泛型类型

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());
}

这段代码没有在impl之后(的尖括号)声明泛型,而是使用了一个具体类型f32。这意味着Point<f32>类型会有一个方法distance_from_origin,而其他T不是f32类型的Point<T>实例则没有定义此方法。

1.4.3. 方法中使用与结构体定义中不同类型的泛型

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<X1, Y1> {
x: X1,
y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
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);
}

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。上例中为Point结构体使用了泛型类型X1Y1,为mixup方法签名使用了X2Y2来使得示例更加清楚。这个方法用selfPoint类型的x值(类型X1)和参数的Point类型的y值(类型Y2)来创建一个新Point 类型的实例。

这个例子的目的是展示一些泛型通过impl声明而另一些通过方法定义声明的情况。这里泛型参数X1Y1声明于impl之后,因为他们与结构体定义相对应。而泛型参数X2Y2声明于fn mixup之后,因为他们只是相对于方法本身的。

1.5. 泛型代码的性能

Rust实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。

Rust通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。我们可以使用泛型来编写不重复的代码,而Rust将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是Rust泛型在运行时极其高效的原因。

2. Trait:定义共同行为

trait定义了一个特定类型所具有的功能,并且这些功能可以与其它类型共享。我们可以使用traits来抽象地定义共享行为,使用trait bounds将泛型指定为具有特定行为的任何类型。

注意:trait 类似于其他语言中的常被称为 接口(interfaces)的功能,虽然有一些不同。

2.1. 定义 trait

trait是一种将方法签名(method signatures)组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

例如,我们想定义一个library crate aggregator,可以显示存储在NewsArticleTweet实例中的Summary数据。为了实现这个功能,每个类型都需要一个Summary,并且可以通过调用summarize方法来获取这个Summary:

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}
  • trait关键字来声明一个trait,后面是trait的名字,本例中是Summary
  • 声明trait为pub以便依赖这个crate的crate也可以使用这个trait。
  • 在大括号中,我们要声明实现这个trait的类型的方法签名,这些方法描述了这些类型所具有的行为,本例中是fn summarize(&self) -> String;
  • 在方法签名后跟分号,不在大括号中提供具体实现。
  • 每一个实现这个trait的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现Summary trait的类型都拥有与这个签名的定义完全一致的summarize方法。
  • trait体中可以有多个方法:一行一个方法签名,且都以分号结尾。

2.2. 为类型实现 trait

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
}
}
  • impl关键字之后,提供需要实现的trait的名称,接着是for和需要实现trait的类型的名称。
    1
    impl TraitName for TypeName
  • impl块中,使用trait定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现trait方法所拥有的行为

现在library在NewsArticleTweet上实现了Summary trait,crate的用户可以像调用常规方法一样调用NewsArticleTweet实例的trait方法了。唯一的区别是trait必须和类型一起引入作用域以便使用额外的trait方法。

例如:使用aggregator这个library crate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use aggregator::{Summary, Tweet};

fn main() {
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());
}

为一个类型实现trait的条件:类型和trait中至少一个属于本地crate。

  • 例如:在我们的aggregator crate中,可以为类型Tweet实现标准库的trait Display,因为Tweet类型是aggregator crate的本地类型;
  • 例如:在我们的aggregator crate中,可以为标准库的类型Vec<T>实现trait Summary,因为Summary trait是aggregator crate的本地trait;
  • 例如:不可以在aggregator crate中,为标准库类型Vec<T>实现标准库trait Display,因为类型Vec<T>和trait Display都是标准库中定义的,都不属于aggregator crate的本地类型或trait,并不位于本地作用域中。

即,不可以为外部类型实现外部trait

这个限制是相干性(coherence)属性的一部分,或更具体的说,叫做孤儿规则(orphan rule),这样命名是因为它的父类型不存在。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个crate可以分别对相同类型实现相同的trait,Rust将无从得知应该使用哪一个实现。

2.3. 默认实现

有时,trait中的某些或所有方法具有默认行为,而不是要求每种类型对所有方法都进行实现。这样,当我们在特定类型上实现trait时,可以选择保留或者覆盖每个方法的默认行为。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)") // 定义默认行为
}
}

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {} // NewsArticle的summarize方法使用默认行为

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)
}
}

如下代码会打印出:“New article available! (Read more…)”

1
2
3
4
5
6
7
8
9
10
11
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());

默认实现可以调用同一trait中的其它方法,哪怕这个方法没有默认实现。例如:

1
2
3
4
5
6
7
pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

默认实现的方法summarize调用了Summary trait的summarize_author方法,虽然这个方法没有给出默认实现。

用这种方式,trait可以提供很多有用的功能而只提供指定一小部分内容的具体实现。

注意,不可以在重载实现中调用该方法的默认实现。

2.4. trait 作为参数

2.4.1. 函数的参数接收任何实现了指定trait的类型

例:我们可以定义一个函数notify来调用其参数item上的summarize方法,该参数是实现了Summary trait的某种类型。为此可以使用impl Trait语法,像这样:

1
2
3
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}

对于item参数,我们指定了impl关键字和trait名称,而不是具体的类型。该参数支持任何实现了指定trait的类型。在 notify函数体中,可以调用任何来自Summary trait的方法,比如summarize。我们可以传递任何NewsArticleTweet的实例来调用notify。任何用其它如Stringi32的类型调用该函数的代码都不能编译,因为它们没有实现Summary

2.4.2. Trait Bound语法

impl Trait语法适用于直观的例子,它实际上是另外一种较长形式语法的语法糖,这种形式称为trait bound

1
2
3
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}

这与之前的例子相同,不过稍微冗长了一些。trait bound与泛型参数声明在一起,位于尖括号<>中的冒号:后面。

impl Trait很方便,适用于短小的例子;trait bound则适用于更复杂的场景。例如,可以获取两个实现了Summary的参数。使用impl Trait的语法看起来像这样:

1
pub fn notify(item1: &impl Summary, item2: &impl Summary) {

这适用于item1item2允许是不同类型的情况(只要它们都实现了Summary)。不过如果你希望强制它们都是相同类型呢?这只有在使用trait bound时才有可能:

1
pub fn notify<T: Summary>(item1: &T, item2: &T) {

泛型T被指定为item1item2的参数限制,如此传递给参数item1item2值的具体类型必须一致。

2.4.3. 通过+指定多个 trait bound

如果 notify 需要显示 item 的格式化形式,同时也要使用 summarize 方法,那么 item 就需要同时实现两个不同的 trait:DisplaySummary。这可以通过+语法实现:

1
pub fn notify(item: &(impl Summary + Display)) {

+语法也适用于泛型的 trait bound:

1
pub fn notify<T: Summary + Display>(item: &T) {

通过指定这两个trait bound,notify的函数体可以调用 summarize 并使用 {} 来格式化 item

2.4.4. 通过where简化 trait bound

然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 where 从句中指定 trait bound 的语法。所以除了这么写:

1
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

还可以像这样使用 where 从句:

1
2
3
4
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{

这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。

2.5. 返回实现了 trait 的类型

也可以在返回值中使用 impl Trait 语法,来返回实现了某个 trait 的类型:

1
2
3
4
5
6
7
8
9
10
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,
}
}

通过使用 impl Summary 作为返回值类型,我们指定了 returns_summarizable 函数返回某个实现了 Summary trait 的类型,但是不确定其具体的类型。在这个例子中 returns_summarizable 返回了一个 Tweet,不过调用方并不知情。

返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。impl Trait 允许你简单地指定函数返回一个 Iterator 而无需写出实际的冗长的类型。

不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle Tweet 就行不通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,
}
}
}

这里尝试返回 NewsArticleTweet。这不能编译,因为 impl Trait 工作方式的限制。

2.6. 使用 trait bounds 来修复largest函数

回顾之前的largest函数和它产生的编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn largest<T>(list: &[T]) -> T {
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);

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest(&char_list);
println!("The largest char is {}", result);
}

产生的编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

在 largest 函数体中我们想要使用大于运算符(>)比较两个 T 类型的值。这个运算符被定义为标准库中 trait std::cmp::PartialOrd 的一个默认方法。所以需要在 T 的 trait bound 中指定 PartialOrd,这样 largest 函数可以用于任何可以比较大小的类型的 slice。因为 PartialOrd 位于 prelude 中所以并不需要手动将其引入作用域。将 largest 的签名修改为如下:

1
fn largest<T: PartialOrd>(list: &[T]) -> T {

此时编译代码,会出现一些不同的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
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
| move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
| help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
--> src/main.rs:4:18
|
4 | for &item in list {
| ----- ^^^^
| ||
| |data moved here
| |move occurs because `item` has type `T`, which does not implement the `Copy` trait
| help: consider removing the `&`: `item`

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10` due to 2 previous errors

错误的核心是 cannot move out of type [T], a non-copy slice,对于非泛型版本的 largest 函数,我们只尝试了寻找最大的 i32char。像 i32char 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 Copy trait。当我们将 largest 函数改成使用泛型后,现在 list 参数的类型就有可能是没有实现 Copy trait 的。这意味着我们可能不能将 list[0] 的值移动到 largest 变量中,这导致了上面的错误。

为了只对实现了 Copy 的类型调用这些代码,可以在 T 的 trait bounds 中增加 Copy。下面例子中展示了一个可以编译的泛型版本的 largest 函数的完整代码,传递给 largestslice 值的类型实现了 PartialOrdCopy 这两个 trait,例如 i32char

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
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);

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest(&char_list);
println!("The largest char is {}", result);
}

如果并不希望限制 largest 函数只能用于实现了 Copy trait 的类型,我们可以在 T 的 trait bounds 中指定 Clone 而不是 Copy。并克隆 slice 的每一个值使得 largest 函数拥有其所有权。使用 clone 函数意味着对于类似 String 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。

另一种 largest 的实现方式是返回在 sliceT 值的引用。如果我们将函数返回值从 T 改为 &T 并改变函数体使其能够返回一个引用,我们将不需要任何 CloneCopy 的 trait bounds 而且也不会有任何的堆分配。

2.7. 使用 trait bound 有条件地实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,下面例子中的类型 Pair<T> 总是实现了 new 方法并返回一个 Pair<T> 的实例。不过在下一个 impl 块中,只有那些为 T 类型实现了 PartialOrd trait (来允许比较) 和 Display trait (来启用打印)的 Pair<T> 才会实现 cmp_display 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}

impl<T: Display + PartialOrd> Pair<T> {
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);
}
}
}

也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。这个 impl 块看起来像这样:

1
2
3
impl<T: Display> ToString for T {
// --snip--
}

因为标准库有了这些 blanket implementation,我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display

1
let s = 3.to_string();

trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。

在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。

另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

3. 生命周期确保引用有效

还有一种泛型,我们一直在使用它,甚至都没有察觉它的存在,这就是生命周期(lifetimes)

其它泛型确保了一种类型有我们期望的行为,生命周期确保了只要我们需要引用有效,它们就一直有效。

Rust中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含的,并且是被推断出来的,正如大部分时候类型也是可以推断的一样。

当类型有多种可能时,我们必须注明类型。类似地,在引用的生命周期之间的关系有多种可能时,我们也必须注明生命周期。所以Rust需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时使用的引用绝对是有效的。

3.1. 生命周期避免悬垂引用

生命周期的主要目标是避免悬垂引用,悬垂引用会导致程序引用非预期的数据。

例如:

1
2
3
4
5
6
7
8
9
10
{
let r;

{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

注意:上例中声明了没有初始值的变量,所以这些变量存在于外部作用域。乍看之下好像和Rust不允许存在空值相冲突。然而如果尝试在给它一个值之前使用这个变量,会出现编译时错误,这就说明了Rust确实不允许空值。

外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为5的变量x。在内部作用域中,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值。

这段代码不能编译,因为r引用的值在尝试使用之前就离开了作用域。如下是错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:7:17
|
7 | r = &x;
| ^^ borrowed value does not live long enough
8 | }
| - `x` dropped here while still borrowed
9 |
10 | println!("r: {}", r);
| - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

变量x并没有 “存在的足够久”。其原因是 x 在到达第 7 行内部作用域结束时就离开了作用域。不过 r 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,r 将会引用在 x 离开作用域时被释放的内存,这时尝试对 r 做任何操作都不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?这得益于借用检查器。

3.1.1. 借用检查器

Rust编译器有一个借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。将上面例子加上带有变量生命周期的注释:

1
2
3
4
5
6
7
8
9
10
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

这里将 r 的生命周期标记为 'a,将 x 的生命周期标记为 'b。可以看到内部的 'b 块要比外部的生命周期 'a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,但它引用了一个拥有生命周期 'b 的对象。程序拒绝编译,因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短。

下面是一个可以正确编译的例子:

1
2
3
4
5
6
7
8
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+

这里 x 拥有生命周期 'b,比 'a 要大。这就意味着 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的时候也总是有效的。

3.2. 函数中的泛型生命周期

1
2
3
4
5
6
7
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

一旦实现了 longest 函数,上面代码就会打印出 “The longest string is abcd”。注意这个函数获取作为引用的字符串 slice,因为我们不希望 longest 函数获取参数的所有权。

如果这样实现longest,程序不能编译:

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named 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`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 x 还是 y。事实上我们也不知道,因为函数体中 if 块返回一个 x 的引用而 else 块返回一个 y 的引用。

当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 if 还是 else 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像之前例子那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 xy 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系,以便借用检查器可以进行分析。

3.3. 生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短,它描述多个引用的生命周期相互间的关系,但并不影响它们的生命周期。

当我们在函数签名中指定了一个泛型类型的参数,函数就可以接收任何类型;类似地,当我们指定了一个泛型生命周期参数,函数就可以接收有任何生命周期的引用。

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,而且非常短。大多数人默认使用'a。生命周期参数注解位于引用的 & 之后,用空格将引用类型与生命周期注解分隔开。

例:

1
2
3
&i32        // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

单个的生命周期注解本身没有多少意义,因为生命周期注解是为了告诉 Rust 多个引用的泛型生命周期参数之间的关系。例如,如果函数有一个生命周期 'ai32 的引用的参数 first。还有另一个同样是生命周期 'ai32 的引用的参数 second。这两个生命周期注解意味着引用 firstsecond 必须与这泛型生命周期存在得一样久。

3.4. 函数签名中的生命周期注解

现在来看看 longest 函数的上下文中的生命周期。类似于泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号<>中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

这段代码能够编译并会产生我们希望得到的结果。

在这个签名中我们想要表达的限制是,所有(两个)参数和返回的引用的生命周期是相关的,也就是这两个参数和返回的引用存活得一样久。

函数签名表明,对于某些生命周期 'a,函数会获取两个参数,他们都是与生命周期 'a 存在得一样长的string slice。函数会返回一个同样也与生命周期 'a 存在得一样长的string slice。

当具体的引用被传递给longest时,被'a所替代的具体生命周期是x的作用域与y的作用域相重叠的那一部分。换一种说法就是,泛型生命周期'a的具体生命周期等同于xy的生命周期中较小的那一个。因为我们用相同的生命周期参数'a标注了返回的引用值,所以返回的引用值就能保证在xy中较短的那个生命周期结束之前有效。这些关系就是我们希望Rust分析代码时所使用的。

注意:

  • 在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,只是指出了任何不满足这个约束条件的值都将被借用检查器拒绝。
  • longest函数并不需要知道xy具体会存在多久,只需要知道有某个可以被'a替代的作用域将会满足这个签名。

当在函数中使用生命周期注解时,这些注解只出现在函数签名中,而不存在于函数体的任何代码中。生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着Rust编译器的工作变得更简单了。如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分。如果不这么做的话,Rust编译器会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离有问题的地方很多步之外的代码。

让我们看看如何通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用:

1
2
3
4
5
6
7
8
9
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);
}
}

在这个例子中,string1直到外部作用域结束都是有效的,string2则在内部作用域中是有效的,而 result 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 “The longest string is long string is long”。

接下来,让我们尝试另外一个例子,该例子揭示了 result 的引用的生命周期必须是两个参数中较短的那个。下面代码将 result 变量的声明移动出内部作用域,但是将 resultstring2 变量的赋值语句一同留在内部作用域中。接着,使用了变量 resultprintln! 也被移动到内部作用域之外:

1
2
3
4
5
6
7
8
9
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);
}

如果尝试编译会出现如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
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

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为longest函数的参数和返回值都使用了相同的生命周期参数'a

如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的。然而,我们通过生命周期参数告诉Rust的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许上面的代码,因为它可能会存在无效的引用。

3.5. 深入理解生命周期

指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。如下代码将能够编译:

1
2
3
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}

在这个例子中,我们为参数 x 和返回值指定了生命周期参数 'a,不过没有为参数 y 指定,因为 y 的生命周期与参数 x 和返回值的生命周期没有任何关系。

当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。例如:

1
2
3
4
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}

编译错误如下:

1
2
3
4
5
6
7
8
9
10
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error

即便我们为返回值指定了生命周期参数'a,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。

出现的问题是 resultlongest 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 result 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针或者是违反内存安全的行为。

3.6. 结构体定义中的生命周期注解

接下来,我们将定义包含引用的结构体,这需要为结构体定义中的每一个引用添加生命周期注解。下面例子中有一个存放了一个string slice 的结构体ImportantExcerpt

1
2
3
4
5
6
7
8
9
10
11
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,
};
}

这个结构体有一个字段,part,它存放了一个string slice,这是一个引用。类似于泛型参数类型,我们必须在结构体名称后面的尖括号<>中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着ImportantExcerpt的实例不能比其 part字段中的引用存在得更久。

main函数创建了一个ImportantExcerpt的实例,它存放了变量novel所拥有的String的第一个句子的引用。novel的数据在 ImportantExcerpt 实例创建之前就存在。另外,直到 ImportantExcerpt 离开作用域之后 novel 都不会离开作用域,所以 ImportantExcerpt 实例中的引用是有效的。

3.7. 生命周期省略(Lifetime Elision)

现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,很多例子中,没有生命周期注解也能编译成功,如:

1
2
3
4
5
6
7
8
9
10
11
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[..]
}

这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名写成这样:

1
fn first_word<'a>(s: &'a str) -> &'a str {

在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。

这里我们提到一些 Rust 的历史是因为可能会有更多明确的模式被合并和添加到编译器中,未来只会需要更少的生命周期注解。

被编码进 Rust 引用分析的模式被称为生命周期省略规则(lifetime elision rules)。这并不是需要程序员遵守的规则,而是一系列特定的场景。编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。

省略规则并不提供完整的推断:如果 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) -> &'a i32
  • 第三条规则:如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明它是一个对象的方法(method),那么所有输出生命周期参数被赋予 self 的生命周期。
    • 这条规则使得方法更容易读写,因为只需更少的符号。

假设我们自己就是编译器,并应用这些规则来计算示上面例子中first_word函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:

1
fn first_word(s: &str) -> &str {

接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为'a,所以现在签名看起来像这样:

1
fn first_word<'a>(s: &'a str) -> &str {

对于第二条规则,因为这里正好只有一个输入生命周期参数,所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:

1
fn first_word<'a>(s: &'a str) -> &'a str {

现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。

在之前的 longest 函数中,我们没有生命周期参数:

1
fn longest(x: &str, y: &str) -> &str {

再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:

1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 self参数。

应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是为什么在编译时这段代码会出现错误:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。

因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看方法签名中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。

3.8. 方法定义中的生命周期注解

例:

1
2
3
4
5
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

(实现方法时)结构体字段的生命周期必须总是在 impl 关键字之后声明,并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。

上例中有一个方法level,它唯一的参数是self的引用,而且返回值只是一个i32,并不引用任何值。impl之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则,我们不必标注self引用的生命周期。

impl块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。

下面是一个适用于第三条生命周期省略规则的例子:

1
2
3
4
5
6
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则,给予&selfannouncement他们各自的生命周期。接着,因为其中一个参数是&self,返回值类型被赋予了&self的生命周期,这样所有的生命周期都被计算出来了。

3.9. 静态生命周期

这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有'static生命周期,我们也可以选择像下面这样标注出来:

1
let s: &'static str = "I have a static lifetime.";

这个字符串的文本被直接储存在程序的二进制文件中,而这个文件总是可用的。因此所有的字符串字面值都是'static的。

你可能在错误信息中见过使用'static生命周期的建议,不过将引用指定为'static之前,思考一下这个引用是否真的在整个程序的生命周期里都有效,以及你是否希望它在整个程序的生命周期里都有效。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 ‘static 的生命周期。

3.10. 结合泛型类型参数、trait bounds 和生命周期

让我们简要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}
}

ann的类型是泛型T,它可以被放入任何实现了where从句中指定的Display trait的类型。这个额外的参数会使用{}打印,这也就是为什么Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数'a和泛型类型参数T都位于函数名后的同一尖括号列表中。

参考资料

[1] 泛型、Trait 和生命周期:https://kaisery.github.io/trpl-zh-cn/ch10-00-generics.html