搞懂 Rust 生命周期 'a

4 min read

学 Rust 的时候,生命周期大概是最让人头疼的概念之一。'a 这个语法看着就不友好,而且很容易让人产生”这东西到底有什么用”的困惑。这篇文章是我自己理解生命周期之后的整理,尽量用直白的方式说清楚。

生命周期解决什么问题

核心就一件事:编译期防止悬垂引用

引用不能活得比它指向的数据更久。

就这么简单。生命周期不是”延长引用寿命”的工具,恰恰相反,它是用来限制引用不能活得太久的。


'a 不是”时间”,是”关系”

刚接触的时候很容易觉得 'a 表示”这个引用能活多久”,其实不对。'a 表达的是多个引用之间的存活关系

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str

这个签名的意思不是”返回值活很久”,而是:

返回的引用,不会比 s1s2较短的那个活得更久。

所以 'a 是一个关系约束,不是具体的时间长度。


什么时候必须写

判断标准很实际:函数返回一个引用,但这个引用可能来自多个输入参数。

能省略的情况——返回值只来自一个输入:

fn first(s: &str) -> &str {
    s
}

必须写的情况——返回值来源不确定:

fn longest(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

编译器不知道返回的引用到底依赖 s1 还是 s2,所以需要你显式告诉它。


结构体里放引用

结构体如果包含引用,就必须标注生命周期:

struct User<'a> {
    name: &'a str,
}

这意味着 User<'a> 的实例不能活得比 'a 更久——它被内部的引用”拴住”了。

实际写代码的时候,结构体里放引用经常带来一连串的生命周期标注。除非你确实需要零拷贝,否则直接用 String 省心很多:

设计特点
struct User<'a> { name: &'a str }零拷贝,但生命周期传染
struct User { name: String }有拷贝开销,但好用

'static 不是”永生”

一个常见误解是 'static 意味着”活到程序结束”。更准确的说法是:

'static = 这个值不依赖任何短生命周期的引用

let s: &'static str = "hello"; // 字面量在程序的静态数据区

字符串字面量天然是 'static 的,因为它编译进了二进制文件。


T: 'static 在约束什么

fn foo<T: 'static>(t: T) {}

不是说 T 会一直存在或者是全局变量。它的意思是 T 内部不能包含指向短生命周期数据的引用

类型满足 T: 'static
i32
String✅ 拥有数据
&str(来自参数)
Vec<String>
Vec<&str>

async 和生命周期

这两个东西搅在一起的时候最让人头疼。原因是:

async fn foo(s: &str) { ... }

等价于:

fn foo<'a>(s: &'a str) -> impl Future<Output = ()> + 'a

async fn 返回的 Future 会把用到的引用”存”在自己里面。本质上 Future 就是一个带引用的结构体,生命周期规则跟前面写 struct User<'a> 完全一样。

这也解释了为什么 tokio::spawn 几乎总要求 'static——spawn 出去的任务可能比当前函数活得久,所以 runtime 要求 Future 不能捕获任何短生命周期引用。


trait 和 dyn Trait 里的生命周期

impl<'a> Iterator for MyIter<'a> {
    type Item = &'a T;
}

意思是:我返回的引用来自内部借用的数据,所以它的生命周期不会超过 'a

Box<dyn Trait + 'a> 也一样——这个 trait object 内部可能借用了 'a 生命周期的数据,它本身不能比 'a 活得久。


一个统一的判断方式

碰到生命周期的问题,问自己一个问题就够了:

这个值内部有没有保存对外部数据的引用?

有 → 生命周期会出现,不管是显式还是隐式的。没有 → 大概率满足 'static

生命周期不是 Rust 额外加的规则,而是把其他语言里隐含的内存依赖关系强制写清楚了。C/C++ 里这些依赖关系一样存在,只是编译器不检查,运行时才爆炸。