本文为阅读《Rust圣经》的第三篇笔记,内容包括《Rust基础入门》8~10章节,阅读本文前建议先阅读本网站的文章《Rust基础入门(一)》《Rust基础入门(二)》,后续章节请移步到本网站文章《Rust基础入门(四)》等,如果想查看原书请搜索“Rust圣经”或点击此链接:[Rust语言圣经(Rust Course)](https://course.rs/about-book.html)


泛型和特征

泛型 Generics

泛型的格式

1
2
3
4
5
6
7
8
9
// 单一泛型参数
fn largest<T>(list: &[T]) -> T {
// ...
}

// 多个泛型参数
fn add<T, U>(a: T, b: T) -> bool {
// ...
}

泛型参数的名称你可以任意起,但是出于惯例,我们都用 T (T 是 type 的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。

注意:泛型的T可以是任何类型,但是上述的这些例子中,并不是所有类型都能比较(比较出largest或比较出 T、U 之间的大小关系),所以,我们需要对参数 T(和 U)加一个类型限制,可以使用std::cmp::PartialOrd 特征(Trait)对 T 进行限制,该特征的目的就是让类型实现可比较的功能。

修改如下(这里的修改还不完整,具体还要再继续学习 Trait 的原理):

1
2
3
4
5
6
7
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
// ...
}

fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
// ...
}

泛型在各个情况下的运用

结构体中使用泛型

举个例子:

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

使用上述泛型的要求是:

  • 提前声明,跟泛型函数定义类似,首先我们在使用泛型参数之前必需要进行声明 Point<T> ,接着就可以在结构体的字段类型中使用 T 来替代具体的类型(即上述例子的前四行)
  • x 和 y 是相同的类型

如果 x 和 y 是不同的类型,那么就要用如下代码:

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

fn main() {
let p = Point{x: 1, y :1.1};
}

枚举中使用泛型

Option 的例子:

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

Option<T> 是一个拥有泛型 T 的枚举类型,它第一个成员是 Some(T),存放了一个类型为 T 的值。得益于泛型的引入,我们可以在任何一个需要返回值的函数中,去使用 Option<T> 枚举类型来做为返回值,用于返回一个任意类型的值 Some(T),或者没有值 None

Result 的例子:

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

这个枚举和 Option 一样,主要用于函数返回值,与 Option 用于值的存在与否不同,Result 关注的主要是值的正确性。

如果函数正常运行,则最后返回一个 Ok(T)T 是函数具体的返回值类型,如果函数异常运行,则返回一个 Err(E)E 是错误类型。例如打开一个文件:如果成功打开文件,则返回 Ok(std::fs::File),因此 T 对应的是 std::fs::File 类型;而当打开文件时出现问题时,返回 Err(std::io::Error)E 对应的就是 std::io::Error 类型。

方法中使用泛型

举个例子:

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>中使用它,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。需要注意的是,这里的 <T> 不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T> 而不再是 Point

除了结构体中的泛型参数,我们还能在该结构体的方法中定义额外的泛型参数,就跟泛型函数一样:

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

上述代码运行结果为:

1
p3.x = 5, p3.y = c

这个例子中,T,U 是定义在结构体 Point 上的泛型参数,V,W 是单独定义在方法 mixup 上的泛型参数,它们并不冲突,说白了,你可以理解为,一个是结构体泛型,一个是函数泛型。

对于具体特定的泛型类型也可以给出其专属的方法:

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

这段代码的方法 distance_from_origin(&self) 只会在 T 恰好为 f32 类型时才可以使用,其它泛型类型则没有定义该方法。

const 泛型

在数组那节,有提到过很重要的一点:[i32; 2] 和 [i32; 3] 是不同的数组类型,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
fn display_array(arr: [i32; 3]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);

let arr: [i32; 2] = [1, 2];
display_array(arr);
}

这段代码会报错

从易到难的解决方案如下:

方案一:使用切片

1
2
3
4
5
6
7
8
9
10
fn display_array(arr: &[i32]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);

let arr: [i32; 2] = [1, 2];
display_array(&arr);
}

局限性:必须用引用

方案二:使用泛型加切片

1
2
3
4
5
6
7
8
9
10
fn display_array<T: std::fmt::Debug>(arr: &[T]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);

let arr: [i32; 2] = [1, 2];
display_array(&arr);
}

要注意的是需要对 T 加一个限制 std::fmt::Debug,该限制表明 T 可以用在 println!("{:?}", arr) 中,因为 {:?} 形式的格式化输出需要 arr 实现该特征。

局限性:还是不能摆脱引用的限制

方案三:使用 const 泛型

1
2
3
4
5
6
7
8
9
10
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);

let arr: [i32; 2] = [1, 2];
display_array(arr);
}

如上所示,我们定义了一个类型为 [T; N] 的数组,其中 T 是一个基于类型的泛型参数,这个和之前讲的泛型没有区别,而重点在于 N 这个泛型参数,它是一个基于值的泛型参数,因为它用来替代的是数组的长度。

N 就是 const 泛型,定义的语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize。

const fn,常量函数

通常情况下,函数是在运行时被调用和执行的。然而,在某些场景下,我们希望在编译期就计算出一些值,以提高运行时的性能或满足某些编译期的约束条件。例如,定义数组的长度、计算常量值等。

有了 const fn,我们可以在编译期执行这些函数,从而将计算结果直接嵌入到生成的代码中。这不仅提高了运行时的性能,还使代码更加简洁和安全。

要定义一个常量函数,只需要在函数声明前加上 const 关键字。例如:

1
2
3
4
5
6
7
8
9
const fn add(a: usize, b: usize) -> usize {
a + b
}

const RESULT: usize = add(5, 10);

fn main() {
println!("The result is: {}", RESULT);
}

虽然 const fn 提供了很多便利,但是由于其在编译期执行,以确保函数能在编译期被安全地求值,因此有一些限制,例如,不可将随机数生成器写成 const fn。

无论在编译时还是运行时调用 const fn,它们的结果总是相同,即使多次调用也是如此。唯一的例外是,如果你在极端情况下进行复杂的浮点操作,你可能会得到(非常轻微的)不同结果。因此,不建议使 数组长度 (arr.len()) 和 Enum判别式 依赖于浮点计算。

以下是一个结合了 const fn 和 const 泛型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Buffer<const N: usize> {
data: [u8; N],
}

const fn compute_buffer_size(factor: usize) -> usize {
factor * 1024
}

fn main() {
const SIZE: usize = compute_buffer_size(4);
let buffer = Buffer::<SIZE> {
data: [0; SIZE],
};
println!("Buffer size: {} bytes", buffer.data.len());
}

程序运行结果为:

1
Buffer size: 4096 bytes

特征 Trait

特征的使用

特征是 Rust 的核心概念之一,它定义了类型之间共享的行为接口。你可以把它理解成其他语言中的接口(Interface)或抽象类,但功能更强大。

如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。

举个例子: 文章 PostWeibo 两种内容载体都可以进行内容总结,那么我们就可以定义一个特征:

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

这里使用 trait 关键字来声明一个特征,Summary 是特征名。在大括号中定义了该特征的所有方法,在这个例子中是: fn summarize(&self) -> String。

特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此,我们只定义特征方法的签名,而不进行实现,此时方法签名结尾是 ;,而不是一个 {}

接下来,每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现 Summary 特征的类型都拥有与这个签名的定义完全一致的 summarize 方法。

Rust 标准库也有一些特征,比如:可以使用 use std::fmt::Display; 将 Rust 标准库中的 Display 特征导入当前作用域,让自定义类型能够实现用户友好的格式化输出。Display 特征的作用是让类型能通过 {} 格式化输出为用户友好的文本形式。

特征的使用示例:

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
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Post {
pub title: String, // 标题
pub author: String, // 作者
pub content: String, // 内容
}

impl Summary for Post {
fn summarize(&self) -> String {
format!("文章{}, 作者是{}", self.title, self.author)
}
}

pub struct Weibo {
pub username: String,
pub content: String
}

impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{}发表了微博{}", self.username, self.content)
}
}

fn main() {
let post = Post{title: "Rust语言简介".to_string(),author: "Sunface".to_string(), content: "Rust棒极了!".to_string()};
let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()};

println!("{}",post.summarize());
println!("{}",weibo.summarize());
}

运行结果为:

1
2
文章 Rust 语言简介, 作者是Sunface
sunface发表了微博好像微博没Tweet好用

上面我们将 Summary 定义成了 pub 公开的。这样,如果他人想要使用我们的 Summary 特征,则可以引入到他们的包中,然后再进行实现。

孤儿规则

如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的。

例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域。

默认实现

就是给出如果不重写方法,该怎么做的一个特征定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub trait Summary {
fn summarize(&self) -> String { // 1
String::from("(Read more...)")
}
}

impl Summary for Post {} // 这里默认使用 1 处方法

impl Summary for Weibo {
fn summarize(&self) -> String { // 重写了方法
format!("{}发表了微博{}", self.username, self.content)
}
}

fn main() {
println!("{}",post.summarize());
println!("{}",weibo.summarize());
}

运行结果为:

1
2
(Read more...)
sunface发表了微博好像微博没Tweet好用

使用特征作为函数参数

(这是特征的重要用法)

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

在这段代码后,你可以使用任何实现了 Summary 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法,例如 summarize 方法。具体的说,可以传递 Post 或 Weibo 的实例来作为参数,而其它类如 String 或者 i32 的类型则不能用做该函数的参数,因为它们没有实现 Summary 特征。

特征约束

上面这个例子的完整写法如下:

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

根据这种完整格式还可以写出以下功能:

1
2
3
4
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
// 使用简化写法,这种写法下两个参数可以是不同类型(简化写法显然做不到限定两个参数为同一类型)
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
// 使用原格式,这种写法下两个参数必须是同一类型

当需要限定多个约束条件,还可以写出如下代码:

1
2
3
pub fn notify(item: &(impl Summary + Display)) {}
// 等价于:
pub fn notify<T: Summary + Display>(item: &T) {}

如果这么写显得复杂,可以进行 where 约束

1
2
3
4
5
6
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
// 等价于:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

下面是一个不同条件下实现方法的例子:

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

cmp_display 方法,并不是所有的 Pair<T> 结构体对象都可以拥有,只有 T 同时实现了 Display + PartialOrd 的 Pair<T> 才可以拥有此方法。

有条件地实现特征,以标准库为任何实现了 Display 特征的类型实现了 ToString 特征为例:

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

let s = 3.to_string(); // 整型实现了 Display

函数返回中的 impl Trait

下列代码:

1
2
3
4
5
6
7
8
fn returns_summarizable() -> impl Summary {
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}

这里的 impl Trait 可以返回一个实现了 Summary 特征的返回值,但不知道是什么类型。这种返回适用于返回的真实类型非常复杂,你不知道该怎么声明时。这种返回值方式有一个很大的限制:只能有一个具体的类型。

泛型和特征的结合

以 largest 函数为例

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
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
// PartialOrd 要求可比较
// Copy 允许按位复制,不转移所有权,因为这里要从切片中取值并赋值
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest = item;
}
}

largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];

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

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

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

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

另一种 largest 的实现方式是返回在 list 中 T 值的引用。如果我们将函数返回值从 T 改为 &T 并改变函数体使其能够返回一个引用,我们将不需要任何 Clone 或 Copy 的特征约束而且也不会有任何的堆分配。

通过 derive 派生特征

derive 是一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。

比如 #[derive(Debug)] 可以为一段代码提供 Debug 派生的特征

生命周期

什么是生命周期

Rust 的生命周期是一种标记系统,用于跟踪引用的有效时间,防止悬垂引用,并在编译时确保内存安全。

悬垂指针和生命周期

生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
#![allow(unused)]
fn main() {
{
let r; // 不初始化它就使用,编译器会给出报错

{
let x = 5;
r = &x;
} // 在这里,x 已经被释放了

println!("r: {}", r); // 对 x 的借用在此处被使用
}
}

这段代码运行会报错 error[E0597]: 'x' does not live long enough

借用检查

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

上面的代码可以看出生命周期 'b'a 小很多。在编译期,Rust 会比较两个变量的生命周期,结果发现 r 明明拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。

如果想要编译通过,也很简单,只要 'b'a 大就好。总之,x 变量只要比 r 活得久,那么 r 就能随意引用 x 且不会存在危险:

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

生命周期标注

生命周期引发的函数报错

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(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
17
18
19
20
fn main() {
// string1 的生命周期从这开始
let string1: String = String::from("abcd"); // 'string1
// string2 是字符串字面量,有 'static 生命周期
let string2: &'static str = "xyz"; // 'static

// 这里编译器不知道应该给 result 分配什么生命周期
let result: &??? = longest(string1.as_str(), string2);
// ^^^^ 问题在这里!
println!("The longest string is {}", result);
} // 'string1 结束

// 这是编译器的视角
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &??? { // 返回谁的生命周期?
if x.len() > y.len() {
x // 可能返回 'a
} else {
y // 可能返回 'b
}
}

在这句话中:

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

编译器需要知道:

  • 返回的引用应该和 x一样长?还是和 y一样长?
  • 因为函数体有 if-else,运行时才知道实际返回哪个

内存图示:

1
2
3
4
5
6
7
8
9
时间轴:
创建 longest调用 释放
string1: |-----------|---------------| (较长生命周期 L1)
string2: |-----------|------| (较短生命周期 L2)
result: |-------------------| ❌ 应该和谁对齐?

// 如果 result 来自 string2,没问题
// 如果 result 来自 string1,也没问题
// 但编译器在编译时不知道 result 来自谁!

这个时候就出现了“在存在多个引用时,编译器有时会无法自动推导生命周期”的现象,需要手动标注。

生命周期标注语法

生命周期标注的语法格式:

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

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

注意:标记的生命周期只是为了取悦编译器,让编译器不要难为我们在通过函数签名指定生命周期参数时,我们并没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过

以下列的函数为例:

1
fn example<'a>(x: &'a str, y: &'a str) -> &'a str

关键点:

  • 需先声明生命周期参数:<'a>
  • 标注表示参数和返回值至少活得和 'a一样久
  • 实际上,'a取所有标注参数生命周期的最小值

注意:生命周期 'a 不代表生命周期等于 'a,而是大于等于 'a

当把具体的引用传给 example 时,那生命周期 'a 的大小就是 xy 的作用域的重合部分,换句话说,'a 的大小将等于 xy 中较小的那个。由于返回值的生命周期也被标记为 'a,因此返回值的生命周期也是 xy 中作用域较小的那个。

一个具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}

这段代码会报错:'string2' does not live long enough

在上述代码中,result 必须要活到 println!处,因为 result 的生命周期是 'a,因此 'a 必须持续到 println!

生命周期的核心思想是:通过标注参数和返回值之间的生命周期关系,让编译器能够静态检查引用的有效性。这不是在指定具体的存活时间,而是在建立一种约束关系,确保:

  1. 返回的引用不会比它引用的数据存活更久
  2. 所有引用在使用时都指向有效的数据

这样Rust就能在编译时防止悬垂引用,同时保持零运行时开销。

结构体中的生命周期

为什么结构体需要生命周期:

1
2
3
4
5
6
7
8
9
// 拥有所有权 - 不需要生命周期
struct Book {
title: String,
}

// 引用类型 - 需要生命周期
struct BookRef<'a> {
title: &'a str,
}

此时生命周期的含义:

1
2
3
struct ImportantExcerpt<'a> {
part: &'a str, // 含义:part引用的字符串必须至少
} // 和ImportantExcerpt实例存活一样久

生命周期消除规则

三个生命周期基本概念:

  1. 输入生命周期:函数参数的生命周期
  2. 输出生命周期:返回值的生命周期
  3. 消除规则:编译器自动添加生命周期的规则

三条核心消除规则:

编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。

  1. 每个引用参数获得独立生命周期

    1
    2
    3
    4
    5
    // 一个参数 → 一个生命周期
    fn foo(x: &i32) // 编译器理解为:fn foo<'a>(x: &'a i32)

    // 两个参数 → 两个生命周期
    fn bar(x: &i32, y: &i32) // 编译器理解为:fn bar<'a, 'b>(x: &'a i32, y: &'b i32)
  2. 只有一个输入生命周期时,赋给所有输出

    1
    2
    3
    4
    5
    6
    // 手写代码
    fn first_word(s: &str) -> &str

    // 编译器应用规则后的理解
    fn first_word<'a>(s: &'a str) -> &'a str
    // 参数和返回值的生命周期相同
  3. 有&self或&mut self时,self的生命周期赋给输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct ImportantExcerpt<'a> {
    part: &'a str,
    }

    impl<'a> ImportantExcerpt<'a> {
    // 手写代码
    fn get_part(&self) -> &str {
    self.part
    }

    // 编译器理解成
    fn get_part(&'a self) -> &'a str {
    // 返回值的生命周期与self相同
    self.part
    }
    }

方法中的生命周期

将泛型的语法和具有生命周期的结构体的方法对比:

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

//具有生命周期的结构体的方法
struct ImportantExcerpt<'a> {
part: &'a str,
}

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

注意:

  1. impl 中必须使用结构体的完整名称,包括 <’a>,因为生命周期标注也是结构体类型的一部分!

  2. 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则

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

静态生命周期 ‘static

'static 生命周期:引用与程序同寿

字符串字面量天然是 ‘static(硬编码进二进制文件

1
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";