每种编程语言都有有效处理概念重复的工具。在Rust中,一个这样的工具就是泛型:具体类型或其他属性的抽象替身。我们可以表达泛型的行为,或者它们如何与其他泛型相关联,而不知道在编译和运行代码时它们的位置会是什么。
函数可以接受某种泛型类型的形参,而不是i32或String等具体类型的形参,这与函数接受带有未知值的形参以在多个具体值上运行相同代码的方式相同。事实上,我们已经在第6章使用了Option
泛型,第8章使用了Vec
和HashMap
,第9章使用了Result
泛型。在本章中,您将探索如何使用泛型定义自己的类型、函数和方法!
首先,我们将回顾如何提取函数以减少代码重复。然后,我们将使用相同的技术从两个仅在形参类型上不同的函数生成一个泛型函数。我们还将解释如何在结构和枚举定义中使用泛型类型。
然后,您将学习如何使用特征trait
以通用的方式定义行为。您可以将特征与泛型类型结合起来,以约束泛型类型只接受具有特定行为的类型,而不是只接受任何类型。
最后,我们将讨论生命周期:向编译器提供关于引用之间如何关联的各种泛型。生命周期允许我们向编译器提供关于借用值的足够信息,以便它能够确保引用在更多的情况下是有效的,而不是在没有我们帮助的情况下。
泛型允许我们用表示多个类型的占位符替换特定类型,以消除代码重复。那么,在深入研究泛型语法之前,让我们先看看如何通过提取一个用表示多个值的占位符替换特定值的函数,以一种不涉及泛型类型的方式删除重复。然后我们将应用相同的技术来提取泛型函数!通过了解如何识别可以提取到函数中的重复代码,您将开始识别可以使用泛型的重复代码。
我们从示例10-1中的短程序开始,该程序查找列表中最大的数字。
10-1:
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);
}
我们在变量number_list
中存储整数列表,并将列表中的第一个数字引用在一个名为最大变量的变量中。然后,我们遍历列表中的所有数字,如果当前数字大于存储在最大的数字,那么在这个变量中替换引用。然而,如果当前的数字小于或等于迄今所见的最大数字,变量就不会改变,代码就会进入列表中的下一个数字。在考虑了列表中的所有数字之后,最大的数字应该指最多的数字,在这个例子中是100。
我们现在的任务是在两个不同的数字列表中找到最大的数字。为此,我们可以选择在示例10-1中重复代码,并在程序中使用相同的逻辑,如示例10-2所示。
10-2
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);
}
尽管该代码工作,重复的代码繁琐,且容易出错。当我们想要改变它的时候,我们也必须记住在多个地方更新代码。
为了消除这种重复,我们将通过定义一个在参数中传递的整数列表来创建一个抽象的函数来创建一个抽象。这个解决方案使我们的代码更加清晰,让我们可以抽象地在列表中找到最大的数字。
在示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 largest
的函数中。然后我们调用函数在示例10-2的两个列表中找到最大的数字。我们也可以在其他i32
值的列表中使用这个函数。
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);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {}", result);
}
largest
函数有一个名为list
的参数,它代表了i32
值的任何切片。因此,当我们调用函数时,代码运行于我们传递的特定值上。
综上所述,以下是我们对从示例10-2更改代码到示例10-3的步骤:
1.识别重复的代码。
2.将重复的代码提取到函数的主体中,并在函数签名中指定该代码的输入和返回值。
3.更新重复代码的两个实例来调用函数。
接下来,我们将使用这些相同的步骤来减少代码重复。同样,函数体可以在抽象list
上操作,而不是特定的值,泛型允许代码在抽象类型上操作。
例如,我们有两个函数:一个在i32
值的切片中找到最大的项,一个在一个char
值中找到最大的项。我们如何消除这种重复?让我们来看看!
我们使用泛型来为函数签名或结构体的项创建定义,然后我们可以使用许多不同的具体数据类型。让我们先来看看如何使用泛型定义函数、结构、枚举和方法。然后我们将讨论generics如何影响代码性能。
当定义一个使用泛型的函数时,我们将泛型放在函数的签名中,我们通常会指定参数和返回值的数据类型。这样做可以使我们的代码更加灵活,并为调用者提供更多的功能,同时防止代码重复。
继续使用我们largest
函数,示例10-4显示了两个函数,它们都找到了一个部分的最大值。然后我们将它们结合成一个使用泛型的单一函数。
10-4
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');
}
larsig_i32
函数是我们在示例10-3中提取的,它在切片中找到了最大的i32
。largest_char
函数可以在切片中找到最大的char
。函数体具有相同的代码,因此,让我们通过在一个函数中引入泛型类型参数来消除重复。
为了在一个新的单个函数中参数化类型,我们需要命名类型参数,就像我们为函数的值参数所做的一样。您可以使用任何标识符作为一个类型参数名。但我们将使用T
,因为根据惯例,在Rust中类型参数名称是短的,通常只是一个字母,而Rust的类型命名约定是CamelCase。Short for “type”,T
是大多数Rust程序员的默认选择。
当我们在函数的主体中使用参数时,我们必须在签名中声明参数名,这样编译器就知道该名称意味着什么。类似地,当我们在函数签名中使用一个类型参数名时,我们必须在使用参数名称之前声明类型参数名。为了定义一般largest
的函数,类型名声明放在函数的名称和参数列表之间的尖括号内<>
内,像这样:
fn largest<T>(list: &[T]) -> &T {
我们这样读这个定义:函数largest
在某个类型的T
上是通用的.这个函数有一个参数命名为list
,它是T
类型的一个切片,largest
函数将返回一个类型T
的值的引用。
示例10-5显示了在其签名中使用泛型数据类型的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,我们将在下一节讨论trait。现在,知道这个错误表明largest
不会为所有可能的 T
类型工作。因为我们想要比较T
类型的值,我们只能使用可以排序的值。为了使比较,标准库有你可以在类型上实现std::cmp::PartialOrd
特性(参见附录C更多的关于这个特性)。通过遵循帮助文本的建议,我们限制对T只适用于实现了PartialOrd
的类型,这个示例将编译,因为标准库在i32
和char
上实现了std::cmp::PartialOrd
。
我们还可以定义structs,使用<>
语法在一个或多个字段中使用泛型类型参数。示例10-6定义了一个10-6
struct,以持有任何类型的x
和y
坐标值。
10-6
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 };
}
在struct定义中使用generics的语法与在函数定义中使用的语法相似。首先,我们在结构体的名称之后,声明了尖括号内的类型参数的名称。然后我们在struct定义中使用泛型类型,否则我们将指定具体的数据类型。
注意,因为我们只使用了一个通用的类型来定义点
,这个定义说点Point
struct在某些
类型中是通用的,而字段x和y都是相同的类型,不管这种类型是什么。如果我们创建一个Point
的实例,它有不同类型的值,如示例10-7中,我们的代码编译不通过。
10-7
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
为了定义一个Point
结构体,x和y都是泛型的,但可以有不同的类型,我们可以使用多个泛型类型参数。例如,在示例10-8中,我们改变了Point
的定义,在T
和U
的类型中是通用的,在x的类型中是T
和y的类型中是U
。
10-8
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 };
}
现在所有的Point
实例都被允许了!您可以根据您想要的定义在定义中使用许多泛型类型参数,但是使用多个使得您的代码难以读取。如果您发现代码中需要大量的泛型类型,则可以表明您的代码需要重组成更小的部分。
正如我们在结构体中所做的那样,我们可以定义enums以在它们的变体中持有通用数据类型。让我们再看一看标准库提供的Option
enum,我们在第六章中使用了它:
enum Option<T> {
Some(T),
None,
}
现在这个定义应该更容易理解了。如你所见 Option
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存任何值的None
。通过 Option
枚举可以表达有一个可能的值的抽象概念,同时因为 Option
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型。第九章使用过的 Result
枚举定义就是一个这样的例子:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚举有两个泛型类型,T
和 E
。Result
有两个成员:Ok
,它存放一个类型 T
的值,而 Err
则存放
一个类型 E
的值。这个定义使得 Result
枚举能很方便的表达任何可能成功(返回 T
类型的值)也可能失败(返回 E
类型的值)的操作。实际上,这就是我们在示例 9-3 用来打开文件的方式:当成功打开文件的时候,T
对应的是 std::fs::File
类型;而当打开文件出现问题时,E 的值则是 std::io::Error
类型。
当你意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。
在为结构体和枚举实现方法时(像第五章那样),一样也可以用泛型。示例 10-9 中展示了示例 10-6 中定义的结构体 Point
,和在其上实现的名为 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,
这样我们就可以使用T
来指定我们在类型Point
上实现方法。通过在impl
后宣布T
为通用类型,Rust可以识别出尖括号中的类型是通用类型,而不是具体类型。我们可以选择这个泛型参数一个不同的名称,而不是在struct定义中声明的泛型参数,但是使用相同的名称是传统的。在一个impl
中编写的方法,它声明泛型类型将在任何类型的实例中定义,不管具体类型最终以通用类型代替。
我们还可以在定义类型的方法时指定对泛型类型的约束。例如,我们可以仅在Point
实例上实现方法,而不是在Point
实例上实现。在示例10-10中,我们使用具体的f32
类型,这意味着我们不会在impl
之后声明任何类型。
10-10
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
这个代码意味着类型Point
将有一个remote ce_from_origin
方法;其他T
不是f32
的Point
的不会有这样的方法定义。该方法测量了我们点从坐标(0.0,0.0)点的距离,并使用只用于浮点类型的数学运算。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中为Point
结构体使用了泛型类型 X1
和 Y1
,为 mixup
方法签名使用了 X2
和 Y2
来使得示例更加清楚。这个
方法用 self
的 Point
类型的 x
值(类型 X1
)和参数的 Point
类型的 y
值(类型 Y2
)来创建一个新 Point
类型的实例:
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);
}
本例的目的是演示一些泛型参数以impl
声明的情况,一些是用方法定义声明的。在这里,通用参数X1
和Y1
在impl
之后被声明,因为它们与struct定义有关。在mixup
后,一般参数X2
和Y2
被声明,因为它们只与该方法相关。
在使用泛型类型参数时,您可能会想知道是否有运行时的成本。好消息是,使用泛型类型不会使程序运行速度比具体类型更慢。
Rust 通过在编译时进行泛型代码的 单态化
(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。让我们看看一个使用标准库中 Option
枚举的例子:
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option
的值并发现有两种Option
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option
展开为 i32
和f64
的两个定义,接着将泛型定义替换为这两个具体的定义。
代码的单态化版本看起来类似于以下(编译器使用不同的名称,而不是我们在这里使用的插图):
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 bounds
来指定一个通用类型可以是任何具有特定行为的类型。
注意:
trait
与其他语言中经常称为接口的特性相似,尽管有一些不同之处。
类型的行为包括我们可以调用的方法。如果我们可以在所有这些类型上调用相同的方法,则不同类型共享相同的行为。trait
定义是一种将签名组合在一起来定义一组实现目标所必需的行为的方法。
例如,这里有多个存放了不同类型和数量文本的结构体:结构体 NewsArticle
用于存放发生于世界各地的新闻故事,而结构体 Tweet
最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个名为 aggregator
的多媒体聚合库crate 用来显示可能储存在 NewsArticle
或 Tweet
实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summarize
方法来请求总结。示例 10-12 中展示了一个表现这个概念的公有 Summary
trait 的定义:
pub trait Summary {
fn summarize(&self) -> String;
}
这里使用 trait
关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 Summary
。我们也声明 trait 为 pub
以便依赖这个 crate 的 crate 也可以使用这个 trait,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是fn summarize(&self) −> String
。
**在方法签名之后,我们没有在大括号内提供一个实现,而是使用分号。**实现此特性的每个类型都必须为该方法的主体提供自己的自定义行为。编译器也会确保任何实现 Summary
trait 的类型都拥有与这个签名的定义完全一致的 summarize
方法。
一个trait
可以在有多个方法:方法签名被列为一行,每一行以分号结束。
trait
现在我们定义了 Summary
trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13中展示了 NewsArticle
结构体上 Summary
trait 的一个实现,它使用标题、作者和创建的位置作为summarize
的返回值。对于 Tweet
结构体,我们选择将 summarize
定义为用户名后跟推文的全部文本
作为返回值,并假设推文内容已经被限制为 280 字符以内。
10-13
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
方法所拥有的行为。
现在库在 NewsArticle
和 Tweet
上实现了Summary trait,crate 的用户可以像调用常规方法一样调用NewsArticle
和 Tweet
实例的 trait
方法了。唯一的区别是 trait
必须和类型一起引入作用域以便使用额外的 trait
方法。这是一个二进制 crate 如何利用 aggregator
库 crate 的例子:
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());
}
This code prints 1 new tweet: horse_ebooks: of course, as you probably already know, people
.
其他依赖 aggregator
crate 的 crate 也可以将 Summar
y 引入作用域以便为其自己的类型实现该 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 将无从得知应该使用哪一个实现。
有时,对于某些或所有方法的默认行为是有用的,而不是在每一种类型上都需要实现所有方法的实现。然后,当我们在特定类型上实现特性时,我们可以保留或覆盖每个方法的默认行为。
示例 10-14 中展示了如何为 Summary
trait 的 summarize
方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
如果想要对 NewsArticle
实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {}
指定一个空的 impl
块。
虽然我们不再直接为 NewsArticle
定义 summarize
方法了,但是我们提供了一个默认实现并且指定NewsArticle
实现 Summary
trait。因此,我们仍然可以对 NewsArticle
实例调用 summarize
方法,如下所示:
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());
为 summarize
创建默认实现并不要求对示例 10-13 中 Tweet
上的 Summary
实现做任何改变。其原因是重写(overriding )一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。
默认实现允许调用相同 trait
中的其他方法,哪怕这些方法没有默认实现。如此,trait
可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 Summary
trait,使其具有一个需要实现的summarize_author
方法,然后定义一个 summarize
方法,此方法的默认实现调用summarize_author
方法:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
为了使用这个版本的 Summary
,只需在实现 trait
时定义 summarize_author
即可:
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
一旦定义了 summarize_author
,我们就可以对 Tweet 结构体的实例调用 summarize
了,而 summarize
的默认实现会调用我们提供的 summarize_author
定义。因为实现了 summarize_author
,Summary
trait 就提供了 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());
注意无法从相同方法的重载实现中调用默认方法。
知道了如何定义 trait
和在类型上实现这些 trait
之后,我们可以探索一下如何使用 trait
来接受多种不同类型的参数。
例如在示例 10-13 中为 NewsArticle
和 Tweet
类型实现了 Summary
trait。我们可以定义一个函数notify
来调用其参数 item 上的 summarize
方法,该参数是实现了 Summary
trait 的某种类型。为此可以使用 impl Trait
语法,像这样:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
对于 item
参数,我们指定了 impl
关键字和trait
名称,而不是具体的类型。该参数支持任何实现了指定 trait
的类型。在 notify
函数体中,可以调用任何来自 Summary
trait 的方法,比如 summarize
。我
们可以传递任何 NewsArticle
或 Tweet
的实例来调用 notify
。任何用其它如 String
或 i32
的类型调用该函数的代码都不能编译,因为它们没有实现 Summary
。
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) {
这适用于 item1 和 item2 允许是不同类型的情况(只要它们都实现了 Summary)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound
时才有可能:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
泛型 T
被指定为 item1 和 item2 的参数限制,如此传递给参数 item1 和 item2 值的具体类型必须一致。
如果 notify
需要显示 item
的格式化形式,同时也要使用 summarize
方法,那么 item 就需要同时实现两个不同的 trait:Display
和 Summary
。这可以通过 +
语法实现:
pub fn notify(item: &(impl Summary + Display)) {
+
语法也适用于泛型的 trait bound:
pub fn notify<T: Summary + Display>(item: &T) {
通过指定这两个 trait bound,notify 的函数体可以调用 summarize
并使用 {}
来格式化 item
。
然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函
数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 where 从句中指定 trait bound 的语法。所以除了这么写:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
we can use a where
clause, like this:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds
的函数很像。
也可以在返回值中使用 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
,但是返回了 NewsArticle
或 `Tweet 就行不通:
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,
}
}
}
这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。第十七章的 ” 为使用不同类型的值而设计的 trait 对象” 部分会介绍如何编写这样一个函数。
traits
来有条件地实现方法通过使用与使用泛型类型参数的impl
块绑定的 trait bound,我们可以有条件地为实现指定trait
的类型实现方法。例如,示例10-15中的Pair
类型总是实现new
函数来返回Pair
的新实例(回想第5章的“定义方法”部分,Self
是impl
块类型的类型别名,在本例中是Pair
)。但是在下一个impl
块中,如果Pair
的内部类型T实现了支持比较的PartialOrd
特征和Display
特征,则它才实现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
特征的任何类型上实现ToString
特征。标准库中的impl
块看起来类似于以下代码:
impl<T: Display> ToString for T {
// --snip--
}
因为标准库有这个覆盖实现,所以我们可以在实现Display
特性的任何类型上调用ToString
特性定义的to_string
方法。例如,我们可以像这样将整数转换为它们对应的String值,因为整数实现了Display:
let s = 3.to_string();
trait
和trait bound
使我们能够编写使用泛型类型参数来减少重复的代码,同时也向编译器指定我们希望泛型类型具有特定的行为。然后编译器可以使用特征绑定信息来检查与我们的代码一起使用的所有具体类型是否提供了正确的行为。在动态类型语言中,如果在没有定义方法的类型上调用方法,则会在运行时得到错误。但是Rust将这些错误转移到编译时,所以我们不得不在代码能够运行之前修复这些问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。
生命周期是我们已经使用过的另一种通用形式。生命周期不是确保类型具有我们想要的行为,而是确保引用在我们需要时有效。
我们在第4章的“引用和借用”一节中没有讨论的一个细节是Rust中的每个引用都有一个生命周期(lifetime),也就是该引用有效的范围。大多数情况下,生命周期是隐式和推断的,就像大多数情况下,类型是推断的一样。只有当可能有多种类型时,才必须对类型进行注释。以类似的方式,当引用的生存期可以以几种不同的方式关联时,我们必须注释生存期。Rust要求我们使用通用的生命周期参数注释关系,以确保在运行时使用的实际引用肯定是有效的。
注释生命周期甚至不是大多数其他编程语言都有的概念,因此这可能会让人感到陌生。尽管我们不会在本章中完整地讨论生命周期,但我们将讨论您可能遇到的生命周期语法的常见方式,以便您能够熟悉这个概念。
生命周期的主要目的是防止悬空引用,悬空引用会导致程序引用的数据不是它打算引用的数据。考虑示例10-16中的程序,它有一个外部作用域和一个内部作用域。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
注意:示例10-16、10-17和10-23中的示例声明变量时没有给它们一个初始值,因此变量名存在于外部作用域中。乍一看,这似乎与Rust没有空值相冲突。但是,如果我们试图在给变量赋值之前使用它,我们将得到一个编译时错误,这表明Rust确实不允许空值。
外部作用域声明了一个没有初始值的名为r
的变量,内部作用域声明了一个名为x
的变量,初始值为5。在内部作用域内,我们尝试将r
的值设置为x
的引用。然后内部作用域结束,我们尝试打印r
中的值。这段代码无法编译,因为在我们尝试使用它之前,r
所引用的值已经超出了作用域。下面是错误信息:
变量x“活得不够长”。原因是当内部作用域在第7行结束时,x将超出作用域。但对于外部作用域,r仍然有效;因为它的范围更大,我们说它“寿命更长”。如果Rust允许这段代码工作,r将引用当x超出作用域时释放的内存,我们试图用r做的任何事情都不会正确工作。那么Rust是如何确定这段代码无效的呢?它使用借用检查器(borrow checker)。
Rust编译器有一个借用检查器,它比较范围以确定是否所有的借用都有效。示例10-17显示了与示例10-16相同的代码,但带有显示变量生命周期的注释。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
}
在这里,我们用'a
注释了r
的生命周期,用'b
注释了x
的生命周期。正如您所看到的,内部的'b
块比外部的'a
生命周期块要小得多。在编译时,Rust比较两个生命期的大小,发现r
的生命期为'a
,但它引用的内存的生命期为'b
。程序被拒绝,因为'b
比'a
短:引用的对象没有引用的生命周期长。
示例10-18修复了代码,使其没有悬空引用,并在编译时没有任何错误。
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
}
在这里,x
的生命周期为'b
,在本例中大于'a
。这意味着r
可以引用x
,因为Rust知道当x
有效时,r
中的引用总是有效的。
现在您已经知道了引用的生存期在哪里,以及Rust如何分析生存期以确保引用始终有效,让我们在函数上下文中探索形参和返回值的泛型生命周期。
我们将编写一个函数,返回两个字符串切片中较长的那个。这个函数将接受两个字符串片,并返回一个字符串片。在我们实现了最长的函数之后,示例10-19中的代码应该打印出he longest string is abcd
。
10-19
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
注意,我们希望函数接受字符串切片,这些字符串切片是引用,而不是字符串,因为我们不希望longest
函数获得其形参的所有权。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
相反,我们得到了以下关于生命周期的错误:
帮助文本显示,返回类型需要一个通用的生命周期参数,因为Rust无法判断被返回的引用是指向x
还是y
。实际上,我们也不知道,因为这个函数体中的if块返回一个指向x
的引用,而else块返回一个指向y
的引用!
当我们定义这个函数时,我们不知道将被传递到这个函数中的具体值,所以我们不知道if
还是else
会执行。我们也不知道将传入的引用的具体生命周期,因此不能像示例10-17和10-18中那样查看作用域,以确定返回的引用是否总是有效。借款检查器也不能确定这一点,因为它不知道x和y的生存期与返回值的生存期之间的关系。要修复此错误,我们将添加定义引用之间关系的通用生命周期参数,以便借用检查器可以执行其分析。
生命周期注释不会改变任何引用的生存时间。相反,它们描述了多个相互引用的生命周期之间的关系,而不影响生命周期。正如函数可以在签名指定泛型类型形参时接受任何类型一样,通过指定泛型生存期形参,函数也可以接受具有任何生存期的引用。
生命周期注释有一种稍微不同寻常的语法:生命周期注释参数的名称必须以撇号('
)开头,并且通常都是小写且非常短,就像泛型类型一样。大多数人使用'a
作为第一个生命周期注释。我们将生命周期参数注释放在引用的&
后面,使用空格将注释与引用的类型分隔开。
这里有一些例子:一个i32
的引用,没有一个生命周期参数,一个i32
的引用,它有一个名为'a
的生命周期参数,而一个i32
的可变引用也有生命周期的'a
。
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
一个生命周期被自身注释没有什么意义,因为注释是为了告诉Rust多个引用的通用生命参数是如何相互关联的。让我们来研究一下在longest
函数的上下文中,如何相互关联。
要在函数签名中使用生命周期注释,我们需要在函数名和参数列表之间声明在尖括号内的生命周期参数,就像我们使用泛型类型参数一样。
在这个签名中我们想要表达的限制是:只要两个参数都有效,返回引用就有效了。这是参数生命周期与返回值生命周期之间的关系。我们将生命周期参数命名为'a
,然后将其添加到每个引用,如示例10-21所示:
10-21
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
函数签名现在告诉Rust,这个函数需要两个参数,它们都是字符串切片,生命周期都是'a
。函数签名还告诉Rust,从函数中返回的字符串切片的生命周期至少是'a
。在实践中,这意味着被longest
函数返回的引用的生命周期与函数参数所引用的值中较小的生命周期一样。这些关系是Rust在分析代码时要使用的。
记住,当我们在这个函数签名中指定生命周期参数时,我们不会改变传递的和返回的任何值的生命周期。相反,我们指定的是借用检查器应该拒绝任何不遵守这些约束的值。请注意, longest
函数并不需要知道 x 和 y 具体会
存在多久,而只需要知道有某个可以被 ’ a 替代的作用域,那么将会满足这个签名。
**当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。**生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着 Rust编译器的工作变得更简单了。如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分。如果不这么做的话,Rust 编译会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离出问题地方很多步之外的代码。
当具体的引用被传递给 longest 时,被 'a
所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期'a
的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a
标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用。示例 10-23 是一个很直观的例子。
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 变量的声明移动出内部作用域,但是将 result 和 string2 变量的赋值语句一同留在内部作用域中。接着,使用了变量 result 的 println! 也被移动到内部作用域之外。注意示例 10-24 中的代码不能通过编译:
10-24 :
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);
}
$ 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 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。
请尝试更多采用不同的值和不同生命周期的引用作为 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 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
出现的问题是 result
在 longest
函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 result
的引用。无法指定生命周期参数来改变悬空引用,而且 Rust 也不允许我们创建一个悬空引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬空指针亦或是违反内存安全的行为。
目前为止,我们只定义过有所有权类型的结构体。接下来,我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体ImportantExcerpt
:
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
,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 ImportantExcerpt
的实例不能比其 part
字段中的引用存在的更久。
这里的 main
函数创建了一个 ImportantExcerp
t 的实例,它存放了变量 novel
所拥有的 String
的第一个句子的引用。novel
的数据在 ImportantExcerpt
实例创建之前就存在。另外,直到 ImportantExcerpt
离开作用域之后 novel
都不会离开作用域,所以 ImportantExcerpt
实例中的引用是有效的。
您已经了解到,每个引用都有一个生命周期,您需要为使用引用的函数或结构指定生命周期参数。然而,在第四章中,我们在示例4 - 9中有一个函数,在示例10-25中再次显示,它在没有生命注释的情况下编译。
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 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
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
(因为这是一个方法),则将self
的生命周期分配给所有输出生命周期参数。第三条规则使方法更易于读写,因为所需的符号更少。
假设我们是编译器。我们将应用这些规则来计算示例10-25中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 {
现在,这个函数签名中的所有引用都具有生命周期,编译器可以继续进行分析,而不需要程序员在这个函数签名中注释生命周期。
让我们来看另一个例子,这次使用的是在示例10-20中开始使用时没有生命周期期参数的最长函数:
fn longest(x: &str, y: &str) -> &str {
让我们应用第一个规则:每个参数都有自己的生命周期。这次我们有两个参数而不是一个,所以我们有两个生命周期:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
您可以看到,第二条规则不适用,因为输入生命周期不止一个。第三条规则也不适用,因为longest
是一个函数而不是一个方法,所以没有一个形参是self
。在处理完所有三条规则之后,我们仍然没有弄清楚返回类型的生命周期是什么。这就是为什么我们在编译示例10-20中的代码时出现了一个错误:编译器使用了生命周期省略规则,但仍然无法找出签名中引用的所有生命周期。
因为第三条规则实际上只适用于方法签名,我们将在该上下文中研究生命周期,以了解为什么第三条规则意味着我们不必经常在方法签名中注释生命周期。
当我们在具有生命周期的结构体上实现方法时,我们使用与示例10-11所示泛型类型参数相同的语法。在哪里声明和使用生命周参数取决于它们是与结构体字段相关还是与方法参数和返回值相关。
结构体字段的生命周期名称总是需要声明在impl
关键字之后,然后使用在结构名称之后,因为这些生命周期是结构类型的一部分。
在impl
块中的方法签名中,引用可能与结构体字段中的引用的生命周期绑定,或者它们可能是独立的。此外,生命周期省略规则通常使得方法签名中不需要生命周期注释。让我们看一些例子,使用示例10-24中定义的名为ImportantExcerpt
的结构。
首先,我们将使用一个名为level
的方法,它的唯一参数是self
的引用,其返回值是i32
,而i32
不是任何引用:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
在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
,我们可以这样注释:
let s: &'static str = "I have a static lifetime.";
这个字符串的文本直接存储在程序的二进制文件中,它总是可用的。因此,所有字符串字面量的生命周期都是'static
。
您可能会看到在错误消息中使用'static
的建议。但是在将'static
指定为引用的生命周期之前,请考虑一下您的引用是否真的存在于程序的整个生命期中,以及您是否希望它存在。大多数情况下,提示'static
的错误消息是由于试图创建悬空引用或可用生命周期不匹配造成的。在这种情况下,解决方案是修复这些问题,而不是指定'static
。
让我们简单地看看在一个函数中指定泛型类型参数、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
}
}
这是示例10-21中longest
函数,它返回两个字符串片中较长的一个。但是现在它有一个额外的名为ann
的泛型类型T
参数,可以由实现where
子句指定的Display
特征的任何类型填充。这个额外的参数将使用{}
打印,这就是为什么必须使用Display
trait边界的原因。因为生命周期是泛型的一种类型,所以生命周期参数'a
和泛型类型参数T
的声明放在同一个列表中,位于函数名后面的尖括号内。