搞Rust的人都知道,所有权、借用检查和生命周期,这三样东西基本决定了一个人在Rust这条路上走得顺不顺。很多初学者把生命周期当成一个“语法难点”,觉得不就是加几个 'a 嘛,编译报错就往上加。但真正到了自己写项目的时候,碰到那些borrow checker报错,往往还是不知道它到底在抱怨什么。这篇文章我就把生命周期这个东西彻底讲透,从它到底解决什么问题,到实际项目里怎么用,再到报错怎么查,一次说清楚。
说实在的,我见过太多人卡在生命周期上,有人甚至因此放弃了Rust。但其实生命周期并没有那么玄乎,它本质上就是一个编译期的概念,跟运行时一点关系都没有。这篇内容适合正在做Rust语言入门、被借用检查器折磨到头秃的初学者,也适合已经在用Rust做开发、想彻底搞懂生命周期到底是怎么运作的人。我会把原理、案例和排查经验一起给出,尽量让每个人都能照着去解决问题。
1. 生命周期在整个Rust体系里到底扮演什么角色
1.1 所有权、借用检查与生命周期的“三角关系”
Rust的内存安全是靠三兄弟协同完成的。所有权告诉你“这块内存的老板是谁”,决定了内存什么时候被释放;借用检查器告诉你“现在谁能拿着这块内存的钥匙进出”,保证同一时间不会出现“一个人正在改、另一个人正在读”的冲突;而生命周期则是负责划定“这把钥匙什么时候过期”。
打个比方你就懂了。所有权相当于一套房子的房产证,业主有权随时处置这套房子。借用相当于你把房门钥匙临时借给朋友,朋友可以进去住一阵子,但不能把房子卖掉。生命周期则是钥匙上印着的一个有效期限——它告诉所有人和编译器:“这把钥匙到哪天就失效了,过了这个时间点不准再用。”Rust编译器就像是小区门口严格的安保,它会检查每一把钥匙是否在有效期内使用。
这里特别想强调一点:生命周期不是运行时的东西,不会生成任何额外的运行时开销。它就是编译器在编译期做的一道静态分析,用来确保“任何引用都不会指向已经不存在的数据”。这也是Rust能在没有垃圾回收的前提下做到内存安全的核心原因之一。
1.2 悬垂引用:生命周期要解决的最本质问题
如果你写过C或者C++,那么对悬垂指针一定不陌生。你释放了一块内存,但某个指针还保存着那个地址,如果继续用这个指针去读写,轻则读到脏数据,重则直接段错误。这种bug在大型C++项目里极难排查,因为出错的位置往往离释放内存的位置十万八千里,而且不一定每次都崩溃。
Rust的设计目标就是把这个在运行时才暴露的问题提前到编译期解决。生命周期标注就是这个方案的执行工具:它让程序员在代码里明确告诉编译器“这个引用跟那个引用的存活时间有关系”,然后编译器用这些信息去做检查。
举一个最简单的例子:
rust复制fn main() {
let x;
{
let y = 42;
x = &y;
}
// 这里再使用 x 就会报错,因为 y 已经离开作用域被销毁了
println!("{}", x);
}
这段代码如果编译,就是一个典型的悬垂引用。Rust编译器不关心你写没写生命周期标注,它自己就能分析出 x 引用的数据在离开内层代码块后就不存在了,所以直接拒绝编译。这说明生命周期标注本质上不是“必需”的语法,而是给编译器提供更多它无法自己猜出来的信息。
1.3 借用检查器的能力边界
有了上面的例子,你可能想问:既然编译器自己能分析作用域,那还需要人写标注干什么?这个问题问到了关键处。
问题出在函数边界上。当你在一个函数里接受了外部传入的 &str,又返回了一个 &str,编译器根本不知道这个返回的引用到底指向的是传入的参数,还是函数内部创建的局部变量,又或者是某个全局数据。它没法“看到”函数内部的实现逻辑和外部调用方之间的关系,这个时候就需要程序员用生命周期标注来建立桥梁。
我记得自己第一次写这样的函数时,编译器报错说“missing lifetime specifier”,当时的反应是:我在函数里干活的,编译器怎么连这都猜不出来?后来才明白,借用检查器不是猜不出来,而是它必须基于明确的规则来验证,不能靠“猜”。生命周期标注写的其实就是约束条件,让编译器能在更大的程序范围内验证“引用不会越界”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 生命周期标注的语法与三条省略规则
2.1 生命周期标注的基本语法形式
生命周期标注的语法非常简洁,就是一个单引号加小写字母,常见的是 'a、'b、'c。它本身没有任何含义,只是一个名字,用来表示“某个作用域”或者“某段存活时间”。
实际写的时候有三种比较常见的形式:
rust复制&'a str // 这是一个带生命周期 `'a` 的不可变引用
&'a mut str // 这是一个带生命周期 `'a` 的可变引用
<'a> // 这是生命周期参数的声明位置,用在函数名后面、结构体名后面、impl 后面
先看一个函数签名的实际例子:
rust复制fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这个签名表达的意思是:x 和 y 这两个引用的生命周期都至少是 'a,返回值也是一个生命周期为 'a 的引用。换句话说,返回值的有效时间不会超过 x 和 y 中任何一个的有效时间。
用更简单的话来说,'a 其实是取 x 和 y 生命周期中“较短的那个”。Rust编译器会想办法让这个约束成立——如果调用方传入的两个引用的存活时间不一样,程序就会用较短的存活时间来统一。这样就能保证无论如何,返回值不会被使用超过它实际引用数据的存续期。
2.2 三条生命周期省略规则
Rust的编译器在设计时做了一个非常体贴的决定:在绝大多数情况下,不需要你手动写生命周期标注,它可以自动帮你补全。这个自动补全的规则就是生命周期省略规则,一共三条。
第一条:每个输入位置上的引用参数,都会分配一个独立的生命周期参数。比如有一个函数 fn foo(x: &str, y: &str),编译器会自动给它补全成:
rust复制fn foo<'a, 'b>(x: &'a str, y: &'b str)
第二条:如果输入位置只有一个生命周期参数,那么这个生命周期会分配给所有输出位置的引用。比如:
rust复制fn first_word(s: &str) -> &str
编译器会补全成:
rust复制fn first_word<'a>(s: &'a str) -> &'a str
这条规则也很好理解:函数只有一个引用输入,那返回的引用大概率就指向这个输入的数据,所以直接把输入的生命周期赋予输出。
第三条:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self(也就是方法调用),那么 self 的生命周期会分配给所有输出位置的引用。这条规则主要方便结构体方法,避免写一堆重复的参数。
这三条规则是Rust标准库十几年来沉淀下来的设计选择,目的就是让90%以上的常用代码不需要写任何生命周期标注,让代码干净好读。需要手动标注的场景,基本都是不满足这三条规则的特殊情况。
2.3 什么时候编译器会“翻脸”要求你手动标注
这里我把最常见的几种情况列出来,你可以对照看一下自己写的代码是否踩过雷:
第一种,多个输入引用,返回值是其中一个。就像前面写的 longest 函数。编译器看到多个输入,按照省略规则会分配给每个输入不同的生命周期参数,返回的时候它不知道应该用哪一个,就会要求你手动指定。
第二种,函数内部创建数据然后返回它的引用。这种直接就是错误的,不是标注能解决的。因为函数一结束,内部创建的数据就被销毁了,返回引用就是悬垂引用。遇到这种情况,应该考虑返回 String 或者 Vec<T> 这类拥有所有权的类型,而不是返回引用。
第三种,结构体里存引用。结构体不是函数,不能享受省略规则,必须显式声明生命周期参数。这个我后面专门开一节来讲。
我早期写Rust时有个不好的习惯,一看到报错就无脑加 <'a>。后来发现这样反而更容易把自己绕晕。正确的做法是:先自己分析返回值和参数之间的关系,想清楚了再写标注。标注写出来是你给编译器的“承诺”,不是用来哄编译器开心的咒语。
3. 核心实操:结构体、impl块中的生命周期运用
3.1 为什么要给结构体加生命周期参数
结构体里面存引用的时候,必须要声明生命周期参数。这条规则很多人第一个月完全想不通,觉得“在函数里能省略,为什么结构体不行?”
原因在于:结构体类型本身是有可能存到别的地方去的,比如塞进 Vec、Option,或者作为另一个结构体的字段。这时候编译器必须确切地知道,这个结构体里的引用到底能活多久,才能保证不出悬垂问题。省略规则只适用于函数和方法的签名推断,无法外推到类型定义上。
一个典型场景是处理文本解析:
rust复制struct WordIter<'a> {
content: &'a str,
pos: usize,
}
impl<'a> WordIter<'a> {
fn new(content: &'a str) -> Self {
WordIter { content, pos: 0 }
}
fn current_word(&self) -> &'a str {
&self.content[self.pos..]
}
}
这里 WordIter 借用了一段字符串切片,字段 content 保存的是对这个字符串的引用。'a 的意思是这个结构体实例存活的时间不能超过它借用的那段字符串。反过来,只要字符串还活着,你就可以继续通过 WordIter 来访问切片数据。
这种模式在Rust里非常常见,尤其是写解析器、遍历器、迭代器这一类组件的时候。我给自己的提示是:每当你发现一个结构体里需要保存 &T 或者 &str 时,第一反应就应该是“这个结构体需要加生命周期参数”。
3.2 一个生命周期参数和多个生命周期参数的取舍
有时候一个生命期参数不够用。比如一个结构体里有两个引用字段,分别来自不同的数据源,它们的存活时间完全独立。这时候就需要分别标注:
rust复制struct TextPair<'a, 'b> {
left: &'a str,
right: &'b str,
}
这里如果图省事只用同一个 'a,虽然也能编译,但会给使用者带来不必要的约束——它强制要求 left 和 right 引用的数据必须有相同的存活时间。如果实际数据本来可以一个活得很长、一个活得很短,这种不必要的约束会让代码在调用方出现“生命周期不够长”的报错。
我个人的经验是:一开始可以多拆几个生命周期参数,让约束尽量少。等代码写完之后,再尝试合并那些确实总是同时出现的生命周期参数。这样比一上来就用一个 'a 勉强编译过去要稳妥得多,因为后者往往会在后续维护时让人抓狂。
3.3 impl块中的生命周期标注与静态生命周期
impl块中声明生命周期参数的时候,语法要特别注意。impl<'a> 后面的 'a,和结构体定义里的 <'a> 是同一个参数,代表着同一种约束。
还有一种比较特殊的生命周期叫 'static,它表示整个程序运行期间都不会被销毁的数据。最常见的 'static 数据就是字符串字面量,比如 let s: &'static str = "hello"。它被直接编译进二进制文件里,所以当然活得和程序一样久。
关于 'static 有一个非常常见的误解:以为它是“最好的生命周期”,能加就加。实际上 'static 是一种极强的约束,它意味着你的数据必须在程序静态区或者通过 Box::leak 等方式主动泄漏,让数据活到程序结束。盲目使用 'static 往往会让设计变得僵硬,而且可能引入内存泄漏。正确的态度是:只有当数据确实需要存活到程序结束,或者你确实要传给某些要求 'static 的API(比如操作系统线程的 spawn)时,才用它。
3.4 方法返回引用时如何和self的生命周期联动
这在写结构体方法时是逃不开的一个点。来看一个比较有代表性的实现:
rust复制struct Container<'a> {
items: &'a [String],
}
impl<'a> Container<'a> {
fn get_first(&self) -> &str {
&self.items[0]
}
}
这里方法 get_first 的返回类型没有显式写生命周期,它可以正常通过编译,是因为第三条省略规则:方法里只要有 &self,返回值的生命周期就默认跟随 self 的生命周期。
但如果你再写一个方法,返回值不依赖 self,而是依赖传入参数,那省略规则就帮不了你了:
rust复制impl<'a> Container<'a> {
fn pick<'b>(&self, other: &'b str) -> &'b str {
other
}
}
这种返回值和 self 无关的情况,必须用独立的生命周期参数 'b 表示。这是我见过很多人在写结构体方法时容易犯的错误:一边心存侥幸“反正有省略规则”,一边疯狂报错。记住,省略规则只覆盖“返回值来自 self”这种情况。
4. 常见错误信息解读与排查技巧实录
4.1 高频错误:missing lifetime specifier
很多人遇到的第一座大山就是 missing lifetime specifier 这条报错。以前我在编辑器里第一次看到它时,完全不知道错在哪个位置,因为编译器只给了一个裸的 &str 并说缺东西。
这个错误的出现场景非常规律。主要是两类:一类是函数有多个引用参数,返回值也是引用;另一类是结构体字段是引用类型。两种场景本质上都是“编译器无法从上下文猜出生命周期应该是什么”。
应对方法很简单。先看一眼报错所在的代码,如果是函数,分析一下返回值到底来自哪个参数,然后给这个参数和返回值标上同一个生命周期。如果是结构体,直接在结构体名字后面加 <‘a>,在引用字段上加 &'a。
以我自己经常写的文本处理代码为例,一开始是这样的:
rust复制fn split_once(s: &str, delim: char) -> (&str, &str) {
let idx = s.find(delim).unwrap();
(&s[..idx], &s[idx + 1..])
}
这段代码因为在函数内部是通过 s.find 来获取切片位置的,所以本质上返回值引用的是输入参数 s 的数据。由于只有一个输入参数引用,省略规则第二条会自动把 'a 分配给输出,这段代码甚至不需要手动标注就能编译通过。
但如果你改成这样,问题就来了:
rust复制fn choose_longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
这里有两个引用参数,编译器就无法判断返回值属于哪一个输入。这种情况就必须手动加标注。
4.2 高频错误:cannot return reference to local variable
这个报错信息直白得多,意思是“你想返回一个指向函数内部局部变量的引用”。编译器说得很清楚,你不能返回指向局部变量的引用,因为局部变量在函数退出时会销毁,返回的引用就没有意义了。
但很多人会写类似的代码。我曾经在做字符串处理时也犯过:
rust复制fn parse_to_str() -> &str {
let s = String::from("hello");
&s
}
这种错误不是靠加生命周期标注能解决的。解决方式是改函数设计:要么把 String 作为返回值让调用者拥有这份数据,要么让调用者传入一个缓冲区,在函数内部填充。
如果要返回 String,那函数签名就会变成:
rust复制fn parse_to_string() -> String {
let s = String::from("hello");
s
}
这是Rust里一个很重要的设计取舍:什么时候返回引用,什么时候返回值。我现在的经验是,除非返回的引用明显指向某个传入参数,否则优先把数据以拥有所有权的形式返回,省得写一堆生命周期约束还容易出错。
4.3 高频错误:borrowed value does not live long enough
这条报错的本质是“引用的存活时间比数据的存活时间更长”。它经常出现在把引用存进某个结构体,或者闭包捕获引用的场景。
举一个很典型的案例:
rust复制struct Holder<'a> {
data: &'a str,
}
fn create_holder() -> Holder<'static> {
let local = String::from("hello");
Holder { data: &local }
}
这个例子编译不过,是因为 Holder<'static> 要求数据活到程序结束,而 local 在函数结束时就销毁了。这是生命周期约束不一致的典型表现:你承诺的 'static 生命周期,比实际数据的存活时间要长。
这种问题排查的核心思路很清楚,就是“减少不一致”:要么把 'static 改成跟实际数据一致的生命周期,要么不要让局部变量在函数里生成又被外部引用。通常我会用一段注释把每个引用和它指向的数据标出来,一眼就能发现哪个对上哪个对不上。
4.4 从报错信息逆向定位问题的方法
编译器报错时,除了错误描述,右下角还会给出“lifetime may not live long enough”这类提示,以及建议代码块。rustc的错误信息其实是出了名的友好,它会明确标出哪个变量活得太短,哪个引用活得太长。
我在排查生命周期问题时有一个固定的套路。第一步,从报错往下看,找到第一个被标记为“被借用”的变量和“借用者”。第二步,确定被借用变量在哪个作用域结束。第三步,确定借用者最晚什么时候还需要用这个引用。第四步,看两者是否有重叠。
如果借用者的使用范围大于被借用者的存活范围,就会报错“does not live long enough”。这时候有两种解法:一是把被借用者的作用域扩大,让它活得久一点;二是把借用者的使用范围缩小,让它在被借用者销毁之前结束使用。
这里我补一句:如果借用者和被借用者都指向同一个结构体里的字段,那问题往往出在结构体生命周期参数设置上。重新审视一下 <'a> 和 &'a 是否是同一个 'a,还是需要不同的生命周期参数。我自己调试时,最爱犯的错误就是把 'a 用得过于宽泛,导致编译器认为所有东西都必须同时存活。
4.5 工具辅助:rust-analyzer和rustc --explain
遇到生命周期报错,手头有两个工具值得依赖。第一个是 rust-analyzer,在VS Code里装好之后,把鼠标悬停在错误代码上,它会直接显示“borrow checker”推断出的生命周期范围,有时比纯粹的报错信息直观很多。
第二个是 rustc --explain,这是编译器自带的一个文档命令。当你看到一段类似 E0597 的代码时,在终端运行:
bash复制rustc --explain E0597
会看到对应的中文或者英文解释,附带代码示例。我建议每个Rust学习者都养成出错先 --explain 的习惯,它比任何博客教程都更准确地描述了你遇到的这个具体错误。
如果你使用的是VS Code,还可以借助调试工具逐步追踪。在launch.json里配置好cargo项目,就可以在 Rust 代码里面下断点,虽然生命周期是编译期概念,但在排查运行时数据什么时候被Drop掉时非常有帮助。说实话,我之前写嵌入式Rust项目的时候,经常用调试器确认数据的存活时间,比肉眼盯着代码猜要快得多。
5. 进阶场景:async、嵌入式开发中的生命周期挑战
5.1 async函数与生命周期之间剪不断理还乱的关系
如果你开始写异步代码,生命周期问题会变得更加棘手。async fn 返回的是一个实现了 Future 的类型,上一段刚理解了生命周期跟函数作用域的关系,下一秒可能就被 async 里的生命周期搞晕。
一个典型的报错场景是这样的:
rust复制async fn process<'a>(input: &'a str) -> &'a str {
input
}
看着好像没问题,但当你在一个 tokio::spawn 里调用它时,tokio::spawn 要求传入的 Future 满足 'static,也就是说它不能借用任何外部数据。这和你想要的“借用 input 再返回引用”的设计直接冲突。
处理方式无非几种:把 input 转成拥有所有权的类型,比如传入 String;或者用 Arc 包裹数据,让多个异步任务共享同一个堆分配;或者把生命周期显式地声明得很复杂,让 Future 不要求 'static。我个人的倾向是,在 async 代码里尽量减少借用,多用 Arc<String> 或者 Arc<[u8]> 这样的共享所有权结构,不然生命周期问题会被 tokio 运行时放大得很难受。
5.2 嵌入式开发中生命周期带来的资源管理约束
在ESP32这类嵌入式平台上用Rust,生命周期问题有一个显著特点:内存资源极其有限,你无法像在PC上那样随便 clone() 一份数据,也不能随便用 Arc 去堆分配。这时候你更需要精打细算地设计引用关系。
比如在编写嵌入式外设驱动时,一个常见的模式是让外设持有总线锁的可变引用:
rust复制struct MyGpio<'a> {
pins: &'a mut esp_hal::gpio::Output<'static>,
}
这里引用的生命周期会直接影响外设的使用方式。为了保证安全,驱动实例创建时必须从某个更长的作用域“借用”这些映射,使用完立即释放。因为嵌入式往往有严格的中断上下文,如果一个设备驱动在中断处理程序中还被借出,就可能导致数据竞争或死锁。
我踩过一个很典型的坑是,我在嵌入式项目里定义了一个全局的 static mut 设备状态,然后想在多个模块之间共享这个状态,结果生命周期和可变引用搞得我一晚上没睡好。后来改为使用 embassy 这类异步框架外加封装好的 peripheral 引用,把生命周期和所有权的逻辑交给框架管理,代码干净了很多。如果你想投入嵌入式Rust开发,提前熟悉生命周期的借用语义,和熟悉寄存器操作同样重要。
5.3 一个综合排查案例的完整复盘
这里给一个我自己印象很深的综合案例。当时我在写一个简单的 HTTP 解析器,数据从网络读取进来之后,要拆解请求路径、查询参数和正文。我最初的设计希望在解析后的结构体里保存 &str 引用,避免拷贝字符串,于是写出了类似下面的代码:
rust复制struct Request<'a> {
method: &'a str,
path: &'a str,
body: &'a str,
}
fn parse_request(data: &[u8]) -> Request<'_> {
let text = std::str::from_utf8(data).unwrap();
let mut lines = text.lines();
let request_line = lines.next().unwrap();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap();
let path = parts.next().unwrap();
Request {
method,
path,
body: lines.collect::<Vec<_>>().join("\n").as_str(),
}
}
这段代码犯了一个经典错误:body 字段指向的是 lines.collect::<Vec<_>>().join("\n") 这个局部产生的临时字符串,函数返回后字符串就销毁了,返回的引用就是悬垂引用。编译器毫不留情地把它拦了下来。
我当时试过各种加生命周期标注的方法,但都无济于事。最终解决方式是改变设计:让 Request 结构体持有 body 的拥有权:
rust复制struct Request<'a> {
method: &'a str,
path: &'a str,
body: String,
}
fn parse_request(data: &[u8]) -> Request<'_> {
let text = std::str::from_utf8(data).unwrap();
let mut lines = text.lines();
let request_line = lines.next().unwrap();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap();
let path = parts.next().unwrap();
Request {
method,
path,
body: lines.collect::<Vec<_>>().join("\n"),
}
}
这样 method 和 path 仍然借用原始数据,而 body 变成了自己的数据。看起来有点“混合模型”,但实际上非常实用,既减少了大部分拷贝,又让编译器能轻松验证安全性。
这个案例让我深刻体会到:生命周期标注解决的是“引用之间的关系”,但如果你发现自己在一个函数里为了满足生命周期做了大量折腾,往往应该回头审视数据结构设计是否合理。有时候把某个字段换成拥有所有权的类型,问题就直接消失了。
6. 我自己总结出的四个实用经验
第一,写Rust代码时先考虑数据归属,再考虑引用。每当你新建一个结构体,先问自己:这个结构体是真需要借用外部数据,还是可以自己拥有这份数据?如果答案是可以拥有,就直接存 String、Vec<T> 等值类型,别去折腾引用。这是最省心的一条路,也是为什么Rust官方标准库中许多类型都选择拥有数据。
第二,生命周期标注是约束而非文档。它的作用是让编译器能证明安全,不是写给后来人看的笔记。你写下的每个 'a、'b 都意味着某些数据必须在同一个时间段内保持存活。所以标注越少,约束越少,代码越灵活。写生命周期时要像给设计做减法的设计师,而不是给代码做加法的装修工。
第三,不要怕用 clone()。很多Rust初学者好像有某种羞耻感,觉得自己写 .clone() 就是不够优雅。但实际上,在非性能瓶颈处使用 clone() 换取代码可读性和开发效率,是完全合理的工程决策。等性能测试告诉你这里确实需要优化时,再回头用引用改写也不迟。
第四,rustc的报错信息是我们最好的老师。每次报错背后都有一条具体的原因,它的提示信息里甚至会直接给出建议修改的代码块,有时候也会附带小例子。用 rustc --explain 查看完整文档会让你对这条错误的理解上升一个台阶。我见过太多人把报错截图扔进搜索引擎,却忽略了盯在眼前的那份官方解释。
7. 写在最后的一个调试小技巧
分享一个我一直在用的调试技巧吧,在面对复杂的生命周期报错时非常管用。
你可以把程序分解成小实验,把报错的函数复制到一个新的小文件里,设置一个最小可复现的例子,然后刻意增加生命周期参数,从而观察编译器的反应。比如把 &str 换成 &'a str、&'static str,或者把引用类型改为所有权类型,看编译是否会通过。这种“二分法”调试方式,能够一步步锁定问题到底出在哪个引用的归属关系上。
我在Rust开发中踩过无数坑,发现一个规律:凡是让我卡住半小时以上的问题,往往不是生命周期本身的语法问题,而是我的设计把生命周期约束搞得过于复杂。后来我学会了砍需求——什么叫砍需求?就是尽量让数据结构简单、引用关系直接。复杂结构体里能直接放 String 就绝不放 &'a str,多个位置需要共享的数据就考虑 Arc,异步里实在绕不过去就牺牲一点效率换取安全。
Rust的生命周期是一个需要花时间才能真正适应的心智模型,但一旦你建立起这个模型,写出来的代码不仅在编译期是安全的,运行时也非常可靠。希望这篇内容能让你少走一些我走过的弯路。
