9、Rust程序设计语言——泛型、Trait和生命周期
1. 泛型
每种编程语言都有一些工具来处理概念上的重复。在Rust中,工具之一就是泛型generics,它是具体类型或其他属性的抽象占位符。可以描述泛型的行为,或者它们与其他泛型之间的关系,而不需要在编译和运行代码时就提前知道它们具体会被什么替换。
函数可以像接受未知值那样,接收某种泛型类型的参数。
1.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}");
}
上面是寻找整数列表中最大值的代码。如果还有一个整数列表需要寻找最大值,可以把上面的代码复制一份,在程序的两个不同位置使用相同的逻辑。
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}");
}
虽然上面的代码能工作,但复制代码既繁琐又容易出错。更改逻辑,要改所有地方。
为了消除这种重复,可以定义一个以任意整数列表为参数的函数来创建抽象。这个方案让代码更清晰,也能让我们以更抽象的方式表达找出一个列表中最大数字的概念。
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值切片。当我们调用这个函数时,代码会运行在我们传入的具体值上。
1.2 在函数定义中使用泛型
当使用泛型定义函数时,本来在函数签名中指定参数和返回值类型的地方,会改用泛型表示。采用这种技术,使得带代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
将下面的两个寻找最大值的函数改为使用泛型:
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}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {result}");
}
largest_i32用来寻找slice中最大的i32,largest_char函数寻找slice中最大的char,因为两者函数体的代码是一样的,可以定义一个函数,再引进泛型参数来消除这种重复。
为了给这个新函数中的类型做参数化,需要给类型参数命名。任何标识符都可以作为类型参数名。这里使用T,按照惯例,Rust中的类型参数名都很短,通常只有一个字母,而Rust类型名的命名约定是UpperCamelCase。T是type的缩写,也是大多数Rust程序员的默认选择。
如果你要在函数体中使用某个参数,就必须先在函数签名中声明它的名字,让编译器知道这个名字表示什么。当你在函数签名中使用类型参数名时,也必须先声明它。为了定义泛型版的largest函数,我们把类型名声明放在函数名和参数列表之间的尖括号<>里,像这样:
fn largest<T>(list: &[T]) -> &T {
可以把这个定义读作:函数largest对某个类型是泛型的。这个函数有一个名为list的参数,它是由T类型值组成的切片,返回同样类型T的引用。
下面改用泛型实现,但是代码还不能编译:
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}");
}

上面是编译报错,错误信息提示了std::cmp::PartialOrd,这是一个trait,说明了largest的函数体并不能适用于T所有可能的类型。如果我们想在函数体中比较T类型的值,只能使用那些值可以排序的类型。为了支持比较,标准库提供了std::cmp::PartialOrd trait,你可以为类型实现它。要修复上面的代码,只能把T限制为只接受实现了PartialOrd的类型,如此代码就能编译运行,标准库已经为i32和char实现了PartialOrd。
1.3 结构体定义中的泛型
也可以使用]<>语法来定义结构体,让一个或多个字段使用泛型类型参数。
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<T>对于一些类型T是泛型的,而且字段x和y都是相同类型的,无论它具体是何类型。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
上面的代码因为x和y类型不同,编译会报错:

. 使用多个泛型类型参数:
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.4 枚举定义中的泛型
枚举也可以在成员中存放泛型数据类型,Option<T>的定义:
enum Option<T> {
Some(T),
None,
}
Option<T>是一个拥有泛型T的枚举,有两个变体,Some变体存放一个类型T的值,None变体不存放任何值。
Result枚举定义拥有多个泛型参数:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result枚举有两个泛型类型,T和E。它有两个变体,Ok存放一个类型T的值,Err存放一个类型E的值。
1.5 方法定义中的泛型
在方法中使用泛型:
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>上实现的方法中使用T。通过在impl之后声明泛型T,Rust就知道Point的尖括号中的类型是泛型而不是具体类型。可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。如果你在impl中编写一个声明泛型类型的方法,那么该方法将在任何类型的实例上定义,无论最终用什么具体类型来替换泛型类型。
定义方法时也可以为泛型指定限制,为具体泛型实例声明方法,不需要impl后面跟尖括号声明泛型参数。
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
这段代码意味着Point<f32>类型会有一个方法distance_from_origin,而其他T不是f32类型的Point<T>实例则没有定义此方法。
结构体定义的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。
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声明而另一些通过方法定义声明的情况。
1.6 泛型代码的性能
Rust通过在编译时对泛型代码进行单态化monomorphization来实现,单态化就是把泛型代码转换成具体代码的过程,方法是用编译时实际用到的具体类型来填充泛型带代码。
在这个过程中,编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。以Option<T>为例:
let integer = Some(5);
let float = Some(5.0);
当Rust编译这些代码的时候,它会进行单态化。编译器会读取传递给Option<T>的值并发现有两种Option<T>:一个对应i32另一个对应f64。为此,它会将泛型定义Option<T>展开为两个针对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);
}
泛型Option<T>被编译器替换为了具体的定义。因为Rust会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。当代码运行时,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是Rust泛型在运行时极其高效的原因。
2. Trait
trait定义了某个特定类型拥有可能与其他类型共享的功能。可以通过trait以一种抽象的方式定义共同行为。可以使用trait约束指定泛型是任何拥有特定行为的类型。
2.1 定义trait
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,结构体NewsArticle用于存放发生于世界各地的新闻故事,而结构体SocialPost最多只能存放280个字符的内容,以及指示该帖子是最新发布的、转发的还是对另一条帖子的回复的元数据。
我们想要创建一个名为aggregator的多媒体聚合库用来显示可能储存在NewsArticle或SocialPost实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的summarize方法来请求摘要。Summary trait的定义:
pub trait Summary {
fn summarize(&self) -> String;
}
使用trait关键字来声明一个trait,后面是trait的名字。在大括号中声明描述实现这个trait的类型所需要的行为的方法签名。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个trait的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现trait的类型都拥有与trait中方法签名完全一致的方法。
trait体中可以有多个方法:一行一个方法签名且都以分号结尾。
2.2 为类型实现trait
下面的例子分别展示NewsArticle和SocialPost结构体上Summary trait的实现。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}, {}", self.username, self.content)
}
}
在类型上实现trait类似于实现常规方法。区别在于Impl关键字后需要提供trait的名称,接着是for和需要实现trait的类型的名称。在impl块中,使用trait定义中的方法签名,不再后跟分号,而是在大括号中编写函数体来为特定类型实现trait方法所拥有的行为。
现在库在NewsArticle和SocialPost上实现了Summary trait,crate的用户可以向调用常规方法一样调用NewsArticle和SocialPost实例的trait方法了。唯一的区别是trait必须和类型一起引入作用域以便使用额外的trait方法。
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
其他依赖aggregator crate的crate也可以将summary引入作用域以便为自己的类型实现该trait。需要注意的限制是,只有在trait或类型至少一个属于当前crate时,我们才能对类型实现该trait。
但是不能为外部类型实现外部trait。这个限制被称为相干性coherence的程序属性的一部分,或者说是孤儿原则orphan rule,其得名于不存在父类型。这条规则保证了其他人编写的代码不会破坏你的代码,反之依然。没有这条规则,两个crate可以分别对相同类型实现相同的trait,而Rust将无从得知使用哪一个实现。
2.3 使用默认实现
有时为trait中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用。这样当为某个特定类型实现trait时,可以选择保留或重载每个方法的默认行为。
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 availble! {}", article.summarize());
为summarize创建默认实现并不要求SocialPost上的Summary实现做任何改变,原因是重载一个默认实现的语法与实现没有默认实现的trait方法的语法一样。
默认实现可以调用相同trait中的其他方法,哪怕这些方法没有默认实现。如此,trait可以提供很多有用的功能而只需要实现指定一小部分内容。
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 SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
一旦定义了summarize_author,我们就可以对SocialPost结构体的实例调用summarize了,而summarize的默认实现会调用我们提供的summarize_author定义。因为实现了summarize_author,Summary trait就提供了summarize方法的功能,且无需编写更多的代码。
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
无法从一个方法的重写实现中调用与其同名的默认实现。
2.4 使用trait作为参数
可以使用trait来接受多种不同类型的参数:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
对于item参数,我们指定了impl关键字和trait名称,而不是具体类型。该参数支持任何实现了指定trait的类型。在notify函数体中,可以调用任何来自Summary trait的方法,比如summarize。可以传递任何NewsArticle或SocialPost的实例来调用notify。任何用其它如String或i32的类型调用该函数的代码都不能编译,因为它们没有实现Summary。
2.4.1 trait约束语法
impl trait语法更直观,但它实际上是更长形式的trait约束语法的语法糖。
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
这种冗长的写法与上面impl Summary等价,但更为冗长。trait约束与泛型声明在一起,位于尖括号中的冒号后面。
impl Trait很方便,适用于短小的例子。更长的trait约束则适用于更复杂的场景。例如,可以获取两个实现了Summary的参数:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
这适用于item1和item2可以是不同类型的情况,只要它们都实现了Summary。不过,如果我们希望强制两个参数必须具有相同类型,就必须使用trait约束:
pub fn notify<T: Summary>(item: &T, item2: &T) {
泛型T被指定为item1和item2的参数限制,如此传递给参数item1和item2值的具体类型必须一致。
2.4.2 通过+语法指定多个trait约束
可以指定多个trait约束notify在item上既能使用格式化显示,又能使用summarize方法:在notify的定义中,指定item必须同时实现Display和Summary两个trait:
pub fn notify(item: &(impl Summary + Display)) {
+语法也适用于泛型的trait约束:
pub fn notify<T: Summary + Display>(item: &T) {
通过指定这两个trait约束,notify函数体可以调用summarize并使用{}来格式化item。
2.4.3 通过where简化trait约束
然而,使用过多的trait约束也有缺点。每个泛型有其自己的trait约束,所以有多个泛型参数的函数在名称和参数列表之间有很长的trait约束信息,这使得函数签名难以阅读。为此,Rust有一个在函数签名之后的where从句中指定trait约束的语法。不使用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约束的函数很像。
2.5 返回实现了trait的类型
也可以在返回值中使用impl Trait语法,来返回实现了某个trait的类型:
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
通过使用impl Summary作为返回值类型,我们指定了returns_summarizable函数返回某个实现了Summary trait的类型,但是不确定具体类型。
返回一个只是指定了需要实现的trait的类型的能力在闭包和迭代器场景十分有用,后面会介绍闭包和迭代器。闭包和迭代器创建只有编译器知道的类型,或者是非常长的类型。impl Trait允许你简单的指定函数返回一个Iterator而无需写出实际的冗长的类型。
不过,这只适用于返回单一类型的情况。下面的代码返回值类型指定为返回impl Summary,但是返回了NewsArticle或SocialPost就不行:
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 {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}


编译器中impl Trait语法的实现限制不允许返回NewsArticle或SocialPost。后面会讲如何编写一个这样的函数。
2.6 使用trait约束有条件地实现方法
通过使用带有trait约束的泛型参数的impl块,可以有条件地只为那些实现了特定trait的类型实现方法。
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);
}
}
}
上面代码中为所有类型T实现了new方法,只为那些实现了PartialOrd和Display trait的T类型实现了cmp_display方法。
对任何满足特定trait约束的类型实现trait被称为blanket implementations,它们被广泛地用于标准库中。例如,标准库为任何实现了Display trait的类型实现了ToString trait:
impl<T: Display> ToString for T {
// 省略
}
因为标准库有了这些blanket implementation,我们可以对任何实现了Display trait的类型调用由ToString定义的to_string方法。
let s = 3.to_string();
Blanket implementation会出现在trait文档的Implementers部分。
3. 生命周期确保引用有效
生命周期是另一类我们已经使用过的泛型。不同于确保类型有期望的行为,生命周期用于保证引用在我们需要的整个期间内都是有效的。
之前讨论也能用和借用的时候,没有提及的是:Rust中每一个引用都有其生命周期lifetime,也就是引用保持有效的作用域。大部分生命周期是隐含并可以推断的。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联,所以Rust需要我们使用泛型声明周期参数来注明它们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
3.1 悬垂引用
生命周期的主要目标是避免悬垂引用dangling references,后者会导致程序引用了非预期引用的数据。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为5的变量x。在内部作用域,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值。这段代码因为r引用的值在尝试引用之前就离开了作用域:

变量x并没有存在的足够久,其原因是x在到达第7行内部作用域结束时就离开了作用域。不过r在外部作用域仍是有效的;作用域越大我们就说它存在的越久。如果Rust允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都不能正常工作。得益于借用检查器,Rust不允许这段代码。
3.1.1 借用检查器
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}"); // | |
// ----+ |
} // -------------+
&mesp;这里x的生命周期’b比’a要大,意味着r可以引用x:Rust知道r中的引用在x有效的时候也总是有效的。
3.2 函数中的泛型生命周期
编写一个返回两个字符串slice中较长者的函数。这个函数获取两个字符串slice并返回一个字符串slice。
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 }
}

提示词文本揭示了返回值需要一个泛型生命周期参数,因为Rust不知道将要返回的引用是指向x或y。
当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是if还是else会被执行。我们也不知道传入的引用的具体生命周期,不能通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道x和y的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
3.3 生命周期注解语法
生命周期注解并不改变人恶化引用的生命周期的长短,相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。
生命周期注解有一个不太常见的语法:生命周期参数名称必须以撇号开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用’a作为第一个生命周期注解。生命周期注解位于引用的&之后,并有一个空格来将引用类型于生命周期注解分隔开。
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
单个的生命周期注解没有多少意义,因为生命周期注解告诉Rust多个引用的泛型生命周期参数如何相互联系。
3.4 在函数签名中
为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期参数。
我们希望函数签名表达如下的约束:返回的引用只要两个参数都有效,就会一直有效。这就是参数生命周期和返回值生命周期之间的关系。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
函数签名表明对于某些生命周期’a,函数会获取两个参数,它们都是与生命周期’a存在的至少一样长的字符串slice。函数会返回一个同样也与生命周期’a存在的至少一样长的字符串slice。实际含义是longest函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。
记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。
当在函数中使用生命周期注解时,这些注解出现函数签名中,而不存在函数体中的任何代码中。生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着Rust编译器的工作变得简单了。如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分。如果不这么做的话,Rust编译会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离出问题地方很多步之外的代码。
当具体的引用被传递给longest时,被’a所替代的具体生命周期是x的作用域与y的作用域相重叠的那一部分。换一种说法就是泛型生命周期’a的具体生命周期等同于x和y的生命周期中较小的那一个。因为我们用相同的生命周期参数’a标注了返回的引用值,所以返回的引用值就能保证在x和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}");
}
}
string1直到外部作用域结束都是有效的,string2则在内部作用域中是有效的,而result则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码。
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}");
}

错误表明为了保证println!中的result是有效的,string2需要直到外部作用域结束都是有效的。Rust知道这些是因为longest函数的参数和返回值都使用了相同的生命周期参数’a。
作为人类,我们可以直观地发现string1比string2更长,因此result会包含指向string1的引用。因为string1尚未离开作用域,对于println!来说string1的引用仍然是有效的。然而,编译器并不能识别出这种情况。我们通过生命周期参数告诉Rust的是:longest函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许上面的代码,因为它可能存在无效的引用。
3.5 关系
指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将longest函数的实现修改为总是返回第一个参数而不是最长的字符串slice,就不需要为参数y指定一个声明周期:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
为参数x和返回值指定了生命周期参数’a,不过没有为参数y指定,因为y的生命周期与参数x和返回值的生命周期没有任何关系。
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}

错误是result在longest函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个result的引用。无法指定生命周期参数来改变悬垂引用,而且Rust也不允许我们创建一个悬垂引用。在这种情况,最好的解决方法是返回一个有所有权的数据类型而不是一个引用,这样调用者就需要负责清理这个值。
3.6 在结构体定义中
目前为止,结构体全都包含拥有所有权的类型。也可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
这个结构体中只有一个字段part,它存放了一个字符串slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着ImportantExcerpt的实例不能比其part字段中的引用存在的更久。
这里main函数创建了一个ImportantExcerpt实例,它存放了变量novel所拥有的String的第一个句子的引用。novel的数据在ImportantExcerpt实例创建前就存在。直到ImportantExcerpt离开作用域之后novel都不会离开作用域,所以ImportantExcerpt实例中的引用是有效的。
3.7 生命周期省略
每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。在本章之前所有的函数中都没有生命周期注解:
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在明确遵守这些规则的前提下变量的生命周期仍是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。编译器会在可以增加生命周期注解来解决错误问题的地方给出一个错误提示,而不是进行推断或猜测。
函数或方法的参数的生命周期被称为输入生命周期,返回值的生命周期被称为输出生命周期。
编译器采用三条规则来判断引用何时不需要明确的注解。第一个规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于fn定义以及impl块。
第一条规则是编译器为每一个引用参数都分配一个生命周期参数。函数有一个引用参数就有一个生命周期参数,有两个引用参数就有两个生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。
第二条规则是如果只有一个输入生命周期参数,那么它将赋予给所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是&self或&mut self,说明这是个方法,那么所有输出生命周期参数被赋予self的生命周期。第三条规则使得方法更容易读写,因为只需要更少的符号。
假设我们是编译器,应用三条规则来计算first_word函数签名中的引用的生命周期。开始签名中的引用并没有关联任何生命周期:
fn first_word(s: &str) -> &str {
编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期:
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或&mut self,同样不适用。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。
3.8 在方法中定义
当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法。我们在哪里声明和使用生命周期参数,取决于它们是与结构体字段相关还是与方法参数和返回值相关。
实现方法时结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
impl块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。
还是以ImportantExcerpt为例子:
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的生命周期,这样所有的生命周期都被计算出来了。
3.9 静态生命周期
这里还有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有’static生命周期:
let s: &'static str = "I have a static lifetime.";
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是’static的。
你可能在错误信息的帮助文本中见过使用’static生命周期的建议,不过将引用指定为’static之前,思考一下这个引用是否真的在整个程序的生命周期里都是有效,以及你是否希望它存在得这么久。大部分情况中,推荐’static生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个’static的生命周期。
3.10 泛型类型参数、trait约束和生命周期
同时指定泛型类型参数、trait约束和生命周期:
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约束是必须的。因为生命周期也是泛型,所以生命周期参数’a和泛型类型参数T都位于函数名后的同一尖括号列表中。
参考
更多推荐

所有评论(0)