Rust-泛型、trait和生命周期

一、泛型数据类型

泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。

在函数定义中使用泛型

定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
继续我们的largest函数,

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

这里 largest_i32 和 largest_char 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!

为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参 数起一个名字。这里选择了名称 T 。任何标识符都可以作为类型参数名,选择 T 是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。 T 作为 “type” 的缩写是大部分 Rust 程序员的首选。

当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

我们将要定义的泛型版本的 largest 函数的签名看起来像这样:

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

这可以理解为:函数 largest 有泛型类型 T 。它有一个参数 list ,它的类型是一个 T 值的切片。 largest 函数将会返回一个与 T 相同类型的值。

下面显示了largest在其签名中使用泛型数据类型的组合函数定义。该清单还显示了我们如何使用i32值或char值的切片调用函数。请注意,这段代码还不能编译,但我们将在本章后面修复它。

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

如果我们现在编译这段代码,我们会得到这个错误:
$ 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

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

结构体定义中的泛型

同样也可以使用 <> 语法来定义拥有一个或多个泛型参数类型字段的结构体。

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

其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

注意 Point 的定义中只使用了一个泛型类型,我们想要表达的是结构体 Point 对于一些类型 T 是泛型的,而且字段 x 和 y 都是相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 Point 的实例,

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

fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}

在这个例子中,当把整型值 5 赋值给 x 时,就告诉了编译器这个 Point<T> 实例中的泛型 T 是整型的。接着指定 y 为 4.0,它被定义为与 x 相同类型,就会得到一个像这样的类型不匹配错误:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
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

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

如果想要定义一个 x 和 y 可以有不同类型且仍然是泛型的 Point 结构体,我们可以使用多个泛型类型参数。下面代码中,我们修改 Point 的定义为拥有两个泛型类型 TU。其中字段x 是T 类型的,而字段y 是U 类型的:
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 };
}

枚举定义中的泛型数据类型

类似于结构体,枚举也可以在其成员中存放泛型数据类型。

enum Option<T> {
Some(T),
None,
}

换句话说 Option<T> 是一个拥有泛型 T 的枚举。它有两个成员: Some ,它存放了一个类型 T 的值,和不存在任何值的 None 。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值” 是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。

枚举也可以拥有多个泛型类型。Result 枚举定义就是一个这样的例子:

enum Result<T, E> {
Ok(T),
Err(E),
}

Result 枚举有两个泛型类型, TEResult 有两个成员: Ok ,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值。这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作。
打开一个文件的场景:当文件被成功打开 T 被放入了 std::fs::File 类型而当打开文件出现问题时 E 被放入了 std::io::Error 类型。

方法定义中的枚举数据类型

定义中带有泛型的结构体或枚举实现方法。我们在 Point<T> 上定义了一个叫做 x 的方法来返回字段 x 中数据的引用:

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 的尖括号中的类型是泛型而不是具体类型。例如,可以选择为 Point<f32> 实例实现方法,而不是为泛型 Point 实例。
我们使用了具体类型f32,这意味着我们不在之后声明任何类型impl
struct Point<T> {
x: T,
y: T,
}

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

fn main() {
let q = Point{ x: 3f32, y: 10f32 };
q.distance_from_origin();
}

这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin ,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。这个方法计算点实例与另一个点坐标之间的距离,它使用了只能用于浮点型的数学运算符。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。
struct Point<T, U> {
x: T,
y: U,
}

impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
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);
}

在 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。

注意泛型参数 T 和 U 声明于 impl 之后,因为他们与结构体定义相对应。而泛型参数 V 和 W 声明于 fn mixup 之后,因为他们只是相对于方法本身的。

泛型代码的性能

在阅读本部分的内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现泛型的方式意味着你的代码使用泛型类型参数相比指定具体类型并没有任何速度上的损失!

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。

编译器寻找所有泛型代 码被调用的位置并使用泛型代码针对具体类型生成代码。
让我们看看一个使用标准库中 Option 枚举的例子:

let integer = Some(5);
let float = Some(5.0);

当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option 的值并发现有两种 Option<T> :一个对应 i32 另一个对应 f64 。为此,它会将泛型定义 Option<T> 展开为 Option_i32Option_f64 ,接着将泛型定义替换为这两个具体的定义。

编译器生成的单态化版本的代码看起来像这样,并包含将泛型 Option 替换为编译器创建的具体定义后的用例代码:

enum Option_i32 {
Some(i32),
None,
}

enum Option_f64 {
Some(f64),
None,
}

fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}

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

二、trait:定义共享的行为

trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。

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

定义 trait

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

例如,这里有多个存放了不同类型和属性文本的结构体:结构体 NewsArticle 用于存放发生于世界各地的新闻故事,而结构体 Tweet 最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。

我们想要创建一个多媒体聚合库用来显示可能储存在 NewsArticleTweet 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 summary 方法来请求总结。

pub trait Summary {
fn summarize(&self) -> String;
}

在这里,我们使用trait关键字和特征的名称来声明一个特征,Summary在本例中就是这样。在大括号内,我们声明了描述实现此特征的类型的行为的方法签名,在本例中为fn summarize(&self) -> String

在方法签名之后,我们使用分号而不是在大括号内提供实现。实现此特征的每种类型都必须为方法体提供自己的自定义行为。编译器将强制任何具有Summary trait 的类型都将summarize 使用此签名准确定义方法。

一个 trait 在其主体中可以有多个方法:方法签名每行列出一个,每行以分号结尾。

为类型实现 trait

现在我们定义了 Summarizable trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。
下面代码展示了 NewsArticle 结构体上 Summarizable trait 的一个实现, 它使用标题、作者和创建的位置作为 summary 的返回值。对于 Tweet 结构体,我们选择将summary 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。

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

在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl 关键字之后,我们提供需要实现 trait 的名称,接着是 for 和需要实现 trait 的类型的名称。在 impl 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
一旦实现了 trait,我们就可以用与 NewsArticleTweet 实例的非 trait 方法一样的方式调用 trait 方法了:
use chapter10::{self, 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());
}

此代码打印1 new tweet: horse_ebooks: of course, as you probably already know, people

其他依赖 aggregator cratecrate 也可以将 Summary 引入作用域以便为其自己的类型实现该 trait。实现 trait 时需要注意的一个限制是,只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 aggregator crate 的自定义类型 Tweet 实现如标准库中的 Display trait,这是因为 Tweet 类型位于 aggregator crate 本地的作用域中。类似地,也可以在 aggregator crate 中为 Vec<T> 实现 Summary,这是因为 Summary trait 位于 aggregator crate 本地作用域中。

但是不能为外部类型实现外部 trait。例如,不能在 aggregator crate 中为 Vec<T> 实现 Display trait。这是因为 DisplayVec<T> 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性(coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

默认实现

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

下面展示了如何为 Summarize traitsummary 方法指定一个默认的字符串值,而不是像之前那样只是定义方法签名:

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

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

如果想要对 NewsArticle 实例使用这个默认实现,而不是定义一个自己的实现,则可以指定一个空的 impl 块:
impl Summarizable for NewsArticle {}

即便选择不再直接为 NewsArticle 定义 summary 方法了,因为 summary 方法有一个默认实现而且 NewsArticle 被指定为实现了 Summarizable trait,我们仍然可以对 NewsArticle 的实例调用 summary 方法:
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());

此代码打印New article available! (Read more...)
重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让 Summarizable trait 也拥有一个要求实现的 author_summary 方法,接着 summary 方法则提供默认实现并调用author_summary 方法:
pub trait Summary {
fn summarize_author(&self) -> String;

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

要使用这个版本的Summary,我们只需要定义summarize_author 何时在类型上实现 trait:
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}

在我们定义之后summarize_author,我们可以调用结构体的summarize实例 Tweet,并且的默认实现summarize将调用summarize_author我们提供的定义。因为我们已经实现了 summarize_authorSummary特性给了我们summarize方法的行为, 而不需要我们编写更多的代码。
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());

这会打印出 1 new tweet: (Read more from @horse_ebooks...)
注意无法从相同方法的重载实现中调用默认方法。

Trait 作为参数

如何使用trait来定义接受许多不同类型的函数。
我们SummaryNewsArticleTweet类型上实现了Trait 。我们可以定义一个notify函数来调用summarizeitem参数上的方法,该方法是实现Summary trait的某种类型。为此,我们可以使用如下impl Trait 语法:

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

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

Trait Bound 语法

impl Trait 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 trait bound,它看起来像:

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

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

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

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

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

一般类型T指定为的类型item1item2 参数约束功能,使得作为参数传递的值的具体类型item1item2必须是相同的。

使用 + 语法指定多个 Trait bounds(边界)

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

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

+语法也适用于泛型类型的 trait bound
pub fn notify<T: Summary + Display>(item: &T) {

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

更清晰的Trait Brounds 与where规范

使用太多的特征边界有其缺点。每个泛型都有自己的特征边界,因此具有多个泛型类型参数的函数可能会在函数名称与其参数列表之间包含大量特征边界信息,从而使得函数签名难以阅读。出于这个原因,Rust 有替代语法用于where在函数签名后的子句中指定特征边界。所以,而不是写这个:

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

我们可以使用一个where子句,像这样:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{

这个函数的签名不那么杂乱:函数名、参数列表和返回类型靠近在一起,类似于没有很多trait bounds的函数。

返回实现 Train 的类型

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

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,但是返回了 NewsArticleTweet 就行不通:

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 工作方式的限制。

使用 trait bounds 来修复 largest 函数

所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复之前那个使用泛型类型参数的 largest 函数定义了! 当我们将其放置不管的时候,它会出现这个错误:

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

错误如下:
$ 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 的一个默认方法。所以需要在 Ttrait bound 中指定 PartialOrd,这样 largest 函数可以用于任何可以比较大小的类型的 slice。因为 PartialOrd 位于 prelude 中所以并不需要手动将其引入作用域。将 largest 的签名修改为如下:
use std::cmp::PartialOrd;

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

这次当我们编译代码时,我们得到了一组不同的错误:
$ 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 array ,对于非泛型版本的 largest 函数,我们只尝试了寻找最大的 i32 和 char 。像 i32 和 char 这 样的类型是已知大小的并可以储存在栈上,所以他们实现了 Copy trait。当我们将 largest 函数改成使用泛型后,现在 list 参数的类型就有可能是没有实现 Copy trait 的,这意味着我们可能不能将 list[0] 的值移动到 largest 变量中。
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];

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

largest
}

如果并不希望限制 largest 函数只能用于实现了 Copy trait 的类型,我们可以在 Ttrait bounds 中指定 Clone 而不是 Copy ,并克隆切片的每一个值使得 largest 函数拥有其所权。但是使用 clone 函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种 largest 的实现方式是返回切片中一个 T 值的引用。如果我们将函数返回值从 T 改为 &T 并改变函数体使其能够返回一个引用,我们将不需要任何 Clone 或Copytrait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!

使用 trait bounds 有条件的实现方法

通过使用带有 trait bound 的泛型 impl 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,类型 Pair<T> 总是实现了 new 方法,不过只有 Pair<T> 内部的 T 类型实现了 PartialOrd trait 来允许比较和 Display trait 来启用打印,才会实现cmp_display :

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 块看起来像这样:
impl<T: Display> ToString for T { 
// --snip--
}

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

traittrait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

三、生命周期与引用有效性

Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。
它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 r ,而内部作用域声明了一个初值为 5 的变量 x 。在内部作用域中,我们尝试将r 的值设置为一个 x 的引用。接着在内部作用域结束后,尝试打印出 r 的值:

fn main() {
{
let r;

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

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

未初始化变量不能被使用
接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!

外部作用域声明了一个r没有初始值的变量,内部作用域声明了一个x初始值为 5的变量。在内部作用域中,我们尝试将的值设置r为对的引用x。然后内部作用域结束,我们尝试打印r. 这段代码无法编译,因为r在我们尝试使用它之前,所指的值已经超出范围。这是错误消息:

$ 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 是如何决定这段代码是不被允许的呢?这得益于借用检查器。

借用检查器

编译器的这一部分叫做 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。

fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}

我们将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b 。如你所见,内部的 'b 块要比外部的生命周期 'a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现r 拥有生命周期 ‘a ,不过它引用了一个拥有生命周期 'b 的对象。程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小: 被引用的对象比它的引用者存在的时间更短。
fn main() {
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
}

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

函数中的泛型生命周期

让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。一旦我们实现了 longest 函数,示例代码应该会打印出 The longest string is abcd

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

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

注意这个函数获取作为引用的字符串 slice,因为我们不希望 longest 函数获取参数的所有权。
但是下面的代码还不能编译:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

我们得到以下关于生命周期的错误:
$ 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 并不知道将要返回的引用是指向 xy。事实上我们也不知道,因为函数体中 if 块返回一个 x 的引用而 else 块返回一个 y 的引用!

当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 if 还是 else 会被执行。借用检查器自身同样也无法确定,因为它不知道 x 和 y 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。

生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。

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

这里有一些例子:我们有一个没有生命周期参数的 i32 的引用,一个有叫做 ‘a 的生命周期参数的 i32 的引用,和一个生命周期也是 'a 的 i32 的可变引用:

&i32        // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

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

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

现在来看看 longest 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。 在这个签名中我们想要表达的限制是所有(两个)参数和返回的引用的生命周期是相关的,也就是这两个参数和返回的引用存活的一样久。

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

这段代码能够编译并会产生我们希望得到 main 函数的结果。
现在函数签名表明对于某些生命周期 'a ,函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串切片。函数会返回一个同样也与生命周期 'a 存在的一样长的字符串切片。这就是我们告诉 Rust 需要其保证的契约。

通过在函数签名中指定生命周期参数,我们并没有改变任何传入后返回的值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。这个函数并不知道(或需要知道) x 和 y 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名。

当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生 惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。

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

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

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

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

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

接下来,让我们尝试一个 result 的引用的生命周期肯定比两个参数的要短的例子。将 result 变量的声明从内部作用域中移动出来,但是将 result 和 string2 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 result 的 println! 移动到内部作用域之外,就在其结束之后。

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

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

当我们尝试编译这段代码时,我们会得到这个错误:
$ 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 函数返回的引用的生命周期应该与传入参数的生命周期中较 短那个保持一致。因此,借用检查器不允许上面的代码,因为它可能会存在无效的引用。

深入理解生命周期

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

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

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

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

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

即便我们为返回值指定了生命周期参数 'a ,这个实现却编译失败了,因为返回值的生命周 期与参数完全没有关联。这里是会出现的错误信息:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here

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

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

从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,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,
};
}

这个结构体有一个字段, part ,它存放了一个字符串切片,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。此注释意味着实例ImportantExcerpt不能超过它在其part字段中持有的引用。
main此处的函数创建了一个ImportantExcerpt结构体实例,该实例保存了对String变量所拥有的的第一句的引用novel。在 创建实例novel之前,数据已经存在ImportantExcerpt。另外,novel在超出范围之后才ImportantExcerpt超出范围,因此ImportantExcerpt实例中的引用是有效的。
这里的 main 函数创建了一个 ImportantExcerpt 的实例,它存放了变量 novel 所拥有的 String 的第一个句子的引用。

生命周期省略(Lifetime Elision)

在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。
在之前有一个函数,他在没有生命周期注释的情况下编译。

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[..]
}

fn main() {
let my_string = String::from("hello world");

// first_word works on slices of `String`s
let word = first_word(&my_string[..]);

let my_string_literal = "hello world";

// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);

// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}

这个函数在没有生命周期注解的情况下编译的原因是历史性的:在 Rust 的早期版本(1.0 之前),这段代码不会被编译,因为每个引用都需要一个明确的生命周期。那时,函数签名应该是这样写的:
fn first_word<'a>(s: &'a str) -> &'a str {

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

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

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

省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出 一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。

首先,介绍一些定义:函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)

现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。

  • 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
  • 3.如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 &self 或 &mut self ,那么 self 的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。

假设我们自己就是编译器并来计算 first_word 函数的签名中的引用的生命周期。 开始时签名中的引用并没有关联任何生命周期:

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

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

对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &'a str {

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

让我们再看看另一个例子,这次我们从没有生命周期参数的 longest 函数开始:

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

再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 self 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译上面代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。

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

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

当为带有生命周期的结构体实现方法时,其语法依然类似函数中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。
(实现方法时)结构体字段的生命周期必须总是在 impl 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
impl 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用下面定义的结构体 ImportantExcerpt 的例子。
首先,我们将使用一个名为level的方法,它的唯一参数是对的引用 self,其返回值是 i32,它不是对任何东西的引用:

struct ImportantExcerpt<'a> {
part: &'a str,
}

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

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

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,
};
}

impl 之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 self 引用的生命周期。

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

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

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

静态生命周期

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

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

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

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

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

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

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 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。