生命周期
一、基础概念
1. 生命周期的本质:悬空引用预防机制
生命周期(Lifetimes)是 Rust 编译器用来确保引用在有效范围内的机制。它用于防止悬空引用(Dangling References)问题,确保引用指向的数据在引用存在期间不会被销毁。例如:
// 错误示例:试图返回局部变量引用
// 报错:returns a reference to data owned by the current function
/*
fn get_data() -> &String {
let s = String::from("Hello");
&s // s 在函数结束时被销毁,返回引用会导致悬空指针
}
*/
// 正确方案 1:转移所有权(最常用)
fn get_data_owned() -> String {
String::from("Hello")
}
// 正确方案 2:传入引用,返回引用(建立生命周期关联)
fn identity<'a>(input: &'a String) -> &'a String {
input
}
在 Rust 中,借用检查器会确保引用的有效性,防止程序出现不安全的悬空引用。
2. 借用检查器(Borrow Checker)的工作原理
借用检查器通过分析变量的生命周期,确保引用始终在有效范围内 作用域(数据源) - 生命周期(引用) >= 0。它会验证以下规则:
- 不允许在拥有可变借用时同时存在不可变借用。
- 不允许在不可变借用存在时修改数据。
3. 生命周期与作用域的关系
生命周期通常与作用域(scope)密切相关,作用域定义了一个变量或引用的有效区域,而生命周期则追踪这个变量或引用的生存时间,避免悬空引用。每个引用都会有一个生命周期,与它指向的数据的生命周期匹配。
fn main() {
let mut x = 10;
let r;
{
let y = &x; // y 的生命周期在此作用域内
r = y; // 将 y 赋值给 r
} // y 离开了作用域,但 r 还在
// println!("{}", r); // 如果取消注释会报错:因为 r 指向的内容寿命不足
x = 20; // 此时修改 x 是安全的,因为旧的借用 r 已经无效了
println!("x: {}", x);
}
4. 显式标注语法('a)
显式标注并不是为了“告诉编译器这个引用活多久”,而是为了建立输入和输出之间的一致性约束。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let a = String::from("Hello World");
let b = String::from("Hello");
let longest = longest(&a, &b);
println!("longest: {}", longest);
}
此处 'a 是生命周期标注,表示函数返回的引用与传入参数的生命周期相同。
二、生命周期省略规则(Lifetime Elision)
为了避免开发者在所有地方都手动书写 'a,Rust 编译器内置了一套确定性的模式。如果代码符合这些模式,编译器会自动补全生命周期。这被称为“省略”,但生命周期依然客观存在,只是被编译器隐藏了。
1. 输入生命周期与输出生命周期
Rust 提供了生命周期省略规则,简化函数签名。当函数参数和返回值都没有生命周期标注时,编译器会推导出生命周期。
- 输入生命周期 (
Input Lifetimes):函数参数中的引用。 - 输出生命周期 (
Output Lifetimes):函数返回值中的引用。
误以为“省略”意味着“没有约束”?
//fn first_word<'a>(s: &'a str) -> &'a str { ... } // 补全后
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap()
}
/* // 错误示例:编译器无法应用省略规则,因为没有输入引用作为来源
fn produce_str() -> &str {
let s = String::from("hello");
&s // 依然会报错:returns a reference to data owned by the current function
}
*/
fn main() {
let a = String::from("Hello World");
let first = first_word(&a);
println!("first: {}", first);
}
编译器会推导出 first_word函数的生命周期。
2. 函数生命周期省略的三大准则
编译器按照以下顺序尝试推导,如果推导失败(即不符合这三条),则要求手动标注:
- 规则一:每一个是引用的参数都有它自己的生命周期参数(例如
fn(x: &'a i32, y: &'b i32))。 - 规则二:如果只有一个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数。
- 规则三:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,那么 self 的生命周期被赋给所有输出生命周期参数。
// 实践场景:编译器会报错,因为不符合规则二(有两个输入)
/*
fn pick_one(x: &str, y: &str) -> &str {
x
}
*/
// 正确方案:手动介入,指明关联
fn pick_one<'a>(x: &'a str, _y: &str) -> &'a str {
x
}
3. 方法签名中的生命周期省略(&self 与 &mut self)
在 Rust 的面向对象风格代码中,返回的引用通常是结构体自身的一部分。在方法中,返回值的生命周期默认与 self 绑定,这意味着只要你手里拿着方法返回的引用,你就一直持有对整个结构体的借用。
struct Level {
content: String,
}
impl Level {
// 自动应用规则三:fn get_content<'a>(&'a self) -> &'a str
fn get_content(&self) -> &str {
&self.content
}
}
fn main() {
let mut lv = Level { content: "Rust".into() };
let res = lv.get_content(); // res 绑定了 lv 的生命周期
// lv.content.push_str("!"); // 报错:无法修改 lv,因为 res 还在引用它
println!("{}", res);
lv.content.push_str("!"); // 成功:res 在 println 之后不再使用,借用结束(NLL特性)
}
三、函数中的生命周期
函数签名中的生命周期标注是对编译器的一种“承诺”。它并不改变引用的实际寿命,而是描述了输入引用的寿命如何“流向”输出引用。当编译器无法通过省略规则自动推导时,我们需要手动精确定义这种流动关系。
1. 单个与多个生命周期参数
函数可以接收多个生命周期参数,每个生命周期都需要进行标注。例如:
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
如果你给所有参数都标注同一个 'a,编译器会强制将它们关联在一起,取其中的**交集(即最短的那个寿命)**作为结果。这有时会导致过度限制。如果两个输入参数之间没有逻辑上的关联,使用不同的生命周期标注可以给调用者更大的灵活性。
// 情况 A:使用单个生命周期。x 和 y 被锁定在同一个寿命中。
fn pick_x<'a>(x: &'a str, y: &'a str) -> &'a str {
x
}
// 情况 B:使用多个生命周期。返回值只与 x 关联,与 y 无关。
fn pick_x_flexible<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
fn main() {
let x_long = String::from("Long lived");
let res;
{
let y_short = String::from("Short");
// 如果使用 pick_x(x_long, y_short),res 会因为 y_short 而被缩短寿命,报错。
// 使用 flexible 版本,res 的寿命只取决于 x_long。
res = pick_x_flexible(&x_long, &y_short);
} // y_short 在这里销毁,但 res 依然有效
println!("res: {}", res);
}
2. 多个引用输入的约束关系
多个引用参数之间的生命周期关系需要通过显式标注进行约束。有时我们需要表达“引用的引用”或者“参数 A 必须活得比参数 B 久”。这通常通过生命周期约束(Bounds)实现,例如 'a: 'b(读作:’a 至少活得和 ’b 一样久)。
当你需要把一个引用存入另一个引用的目标位置时(例如在处理缓存或修改器时)。
// 需求:将 source 的内容更新到 target 指向的地方
// 约束:'a: 'b 表示 source 的寿命必须覆盖 target 的寿命
fn update_and_return<'a, 'b>(target: &mut &'b str, source: &'a str)
where 'a: 'b
{
*target = source; // 只有 source 活得够久,赋值才是安全的
}
fn main() {
// 1. target 在外层作用域,它的生命周期很长
let mut target: &str = "I am long-lived";
{
// 2. source 在内层作用域,它在大括号结束时就会被销毁
let source_data = String::from("I am short-lived");
let source_ref = &source_data;
// 3. 尝试赋值:
// 这里的 'a 是内部作用域,'b 是外部作用域。
// 显然 'a 并不比 'b 活得久,违反了 'a: 'b。
// 编译器报错:`source_data` does not live long enough
update_and_return(&mut target, source_ref);
}
// 4. 危险:如果上面不报错,这里打印 target 就会访问到已经死掉的内存!
println!("{}", target);
}
当内容和容器在同一个作用域,或者内容活得更久时,代码是安全的。
fn update_and_return<'a, 'b>(target: &mut &'b str, source: &'a str)
where 'a: 'b
{
*target = source;
}
fn main() {
// 1. source 先创建,它的生命周期最长
let source_data = String::from("I live long enough");
let source_ref = &source_data;
// 2. target 后创建(或在同一层)
let mut target: &str = "initial";
// 3. 赋值:
// 此时 source_ref 的寿命 ('a) 覆盖了 target 的寿命 ('b)。
// 满足 'a: 'b,安全通过!
update_and_return(&mut target, source_ref);
// 4. 这里的 target 指向 source_data,而 source_data 还没死,安全。
println!("{}", target);
}
3. 返回引用的生命周期标注要求
返回引用时,必须明确标注返回值生命周期的关系。
- 来源合法性:返回的引用必须来源于输入参数。
- 绝对禁止:严禁返回指向函数内局部变量的引用。
struct Config {
env: String,
port: u32,
}
// 实践:从结构体引用中提取内部字段的引用
// 这里的 'a 建立了“整体”与“部分”的寿命契约
fn get_env_ref<'a>(config: &'a Config) -> &'a str {
&config.env
}
// 错误尝试:如果你试图返回一个跟输入无关的引用
/*
fn mistake<'a>(input: &'a str) -> &'a str {
let s = String::from("I am local");
&s // 即使标注了 'a,编译器也会发现 s 的实际寿命达不到 'a 的要求
}
*/
四、结构体与枚举中的生命周期
当一个结构体或枚举中包含引用时,这个结构体本身就失去了“独立性”。它变成了一个从属品,其寿命被强制绑定到它所指向的数据上。Rust 要求必须在类型定义上显式标注寿命,以提醒调用者:这个结构体不能比它内部的引用活得久。
1. 持有引用的结构体定义
当结构体包含引用时,需要显式标注结构体的生命周期。如果你试图构建一个结构体来存放解析后的数据(如 User 持有 &str 名字),你必须保证 User 在使用时,原始字符串还没被销毁。
struct User<'a> {
name: &'a str, // 结构体持有引用
}
/* fn create_user() -> User<'_> {
let name = String::from("Alice");
let user = User { name: &name };
user // 报错:name 在函数结束时销毁,user 的引用失效
}
*/
fn main() {
let raw_data = String::from("Alice-25-Engineer");
// 假设我们解析这段字符串
let name_slice = &raw_data[0..5];
let user = User { name: name_slice };
println!("User name: {}", user.name);
} // user 和 raw_data 在此处一起安全销毁
2. 持有引用的枚举定义
和结构体类似,枚举类型也需要为引用添加生命周期标注。 枚举的生命周期规则与结构体完全一致。只要有一个变体(Variant)持有了引用,整个枚举就必须带有生命周期标注。
enum Message<'a> {
Quit,
Move { x: i32, y: i32 },
Write(&'a str), // 只要这里有引用,上面定义就得加 <'a>
}
3. impl 块中的生命周期声明
在 impl 块中,生命周期需要明确标注与结构体的关系。例如:
&self vs &'a self
fn method(&self):这是最常见的方法签名,表示对结构体的借用是临时的,只在方法调用期间有效。它不需要显式的生命周期标注,Rust 编译器会根据结构体的生命周期推导出它的有效期。fn method(&'a self):这个签名意味着对结构体的借用必须持续到结构体的生命周期 ’a 结束。这种做法是非常危险的,因为它可能会锁定结构体的借用,直到结构体被销毁为止,导致其他代码无法使用这个结构体。
struct Inspector<'a> {
context: &'a str,
}
impl<'a> Inspector<'a> {
// 正常的方法:借用 &self,用完就还
fn read(&self) {
println!("读取内容: {}", self.context);
}
// 危险的方法:强制借用 self 持续时间为 'a
// 这里的 'a 是 context 的寿命,也是这个结构体能活的最长寿命
fn lock_forever(&'a mut self) {
println!("!!! 锁定整个结构体 !!!");
}
}
fn main() {
let data = String::from("重要卷宗");
let mut detective = Inspector { context: &data };
detective.read(); // 第一次读取:成功
detective.read(); // 第二次读取:成功,说明前两次借用都“还”了
// 关键点:调用这个“终身锁定”的方法
detective.lock_forever();
// --- 观察点 ---
// 按理说,detective 这个变量现在还活着(还没到 main 结尾)
// 但是,如果你尝试再次调用 read():
// detective.read();
// 报错:cannot borrow `detective` as immutable because it is also borrowed as mutable
// 编译器的逻辑:
// 1. lock_forever 要求可变借用 detective 持续 'a 那么久。
// 2. 'a 是 data 的寿命。
// 3. 在 main 结束前,data 一直活着,所以那个可变借用一直没还。
// 4. 一个对象被可变借用期间,不能进行任何其他借用(哪怕是只读)。
}
如果我们将 &’a mut self 改回正常的 &mut self(编译器会自动推导一个临时的生命周期 ’b),你会发现一切都恢复了正常。
struct Inspector<'a> {
context: &'a str,
}
impl<'a> Inspector<'a> {
// 正常的方法:借用 &self,用完就还
fn read(&self) {
println!("读取内容: {}", self.context);
}
// 正常的方法:不标注 'a,使用匿名的临时生命周期
fn quick_work(&mut self) {
println!("快速处理,完事还回权力。");
}
}
fn main() {
let data = String::from("重要卷宗");
let mut detective = Inspector { context: &data };
detective.read();
detective.quick_work(); // 借用开始 -> 方法执行 -> 借用结束(归还权力)
detective.read(); // 成功!因为权限已经还回来了。
detective.quick_work(); // 再次成功。
}
深度总结:为什么 &'a self 这么直观地“坏事”?
你可以这样理解:
-
普通
&self:像你去图书馆借书。看完(方法结束)就把书还给图书馆了。其他人(或你后续的代码)还能再借。 -
&'a self:像你跟图书馆签了个霸王条款:“这书我借了,直到图书馆倒闭('a结束)我才还。”
- 结果:虽然你手里拿着书(变量还在作用域内),但因为你一直占着“借阅名额”不放,图书馆(编译器)认为这本书一直处于“被借出且不可用”状态,导致后续任何尝试访问该书的操作都会报错。
为什么会出现这种代码?
通常是因为开发者想在结构体里存储一个指向自己的引用(自引用),或者在复杂的 trait 嵌套中写错了标注。在 99% 的情况下,你都不应该在方法的 self 前加上结构体的生命周期参数。
4. 结构体字段间的生命周期约束
如果一个结构体有多个引用字段,你可以给它们不同的寿命,或者强制它们同命。
struct Dual<'a, 'b> {
short: &'a str,
long: &'b str, // 独立寿命 'b
}
fn main() {
let s_long: &'static str = "I am static";
let mut res: &str = "";
{
let s_short = String::from("short");
let d = Dual {
short: &s_short,
long: s_long, // 'b 被识别为 'static
};
// 成功!因为 d.long 的寿命 'b 是独立的。
// 编译器知道即使 d 销毁了,d.long 引向的数据依然是 'static 的。
res = d.long;
}
println!("结果: {}", res); // 运行成功:打印 "I am static"
/* {
let s_short = String::from("short");
let d = Dual {
short: &s_short, // 寿命是这个大括号
long: s_long, // 静态寿命被“传染”,降级为大括号寿命
};
// 报错点:虽然 d.long 引用的是静态字符串,
// 但编译器认为 d.long 的寿命受限于 DualSingle<'a> 的 'a。
// 而 'a 已经在刚才被 s_short 锁定为大括号寿命了。
res = d.short;
} // s_short 销毁,'a 结束
println!("{}", res); // 报错:res 借用的数据存活时间不够长
*/
}
五、生命周期约束与绑定
核心逻辑:生命周期约束是 Rust 处理“嵌套关系”和“泛型安全性”的铁律。它通过两种符号:'a: 'b(生命周期比拼)和 T: 'a(类型寿命担保),确保在复杂的嵌套结构中,没有任何一个零件会提前损坏。
1. 生命周期子类型化(Subtyping):'a: 'b
生命周期子类型化表示一个生命周期 'a 至少比 'b 长。在编写如缓存、包装器或视图(View)结构时,你经常会遇到“一个引用指向另一个引用”的情况。这时,必须确保内部引用比外部引用更长命。
❌ 错误演示:内部引用比外部引用早死
struct Buffer<'a>(&'a [u8]);
// 需求:Manager 持有一个 Buffer 的引用。
// 逻辑错误:没有指定 'a 和 'b 的关系。
// 如果 'a (数据源) 比 'b (Manager对Buffer的引用) 先死,Manager 就会崩溃。
struct Manager<'a, 'b> {
buf: &'b Buffer<'a>,
}
fn main() {
let mut manager_ptr: Option<Manager> = None;
{
let data = vec![1, 2, 3];
let buffer = Buffer(&data); // 'a 是 data 的寿命
let m = Manager { buf: &buffer }; // 'b 是 buffer 的寿命
manager_ptr = Some(m);
} // data 在这里被销毁,'a 结束。
// 但 'b 可能还没结束(如果 manager_ptr 还在使用),导致悬空指针。
}
✅ 正确演示:显式声明 'a: 'b
通过约束,编译器会强制要求:你想给 Manager 用的数据,必须比 Manager 本身活得久。
// 约束:'a 必须覆盖 'b (数据源必须比借用它的 Manager 活得久)
struct Manager<'a, 'b>
where 'a: 'b
{
buf: &'b Buffer<'a>,
}
// 这样编译器就能在 main 函数中发现:你给的 Buffer 活不过大括号,
// 从而阻止你把 Manager 赋值给外部的 manager_ptr。
2. 泛型类型生命周期约束:T: 'a
真实含义是:类型 T 中包含的所有引用,都必须活得比 'a 久。 如果 T 是一个不带引用的普通结构体(如 i32),它默认满足任何 'a(因为它永远不会因引用失效而崩溃)。
当你编写一个容器来装载第三方泛型数据时,你必须确保这些数据在容器存在期间是有效的。
✅ 正确演示:RefWrapper 模式
// T: 'a 保证了不管 T 是什么,只要它里面有引用,那些引用就得比 'a 活得久
struct RefWrapper<'a, T: 'a> {
data_ref: &'a T,
}
fn main() {
let val = String::from("hello");
// T 是 String,String 不含引用,天然满足 T: 'static,当然也满足任何 'a
let wrapper = RefWrapper { data_ref: &val };
// 如果 T 是另一个含有引用的结构体,编译器就会检查 T 内部的引用是否覆盖 'a
}
3. Trait 对象生命周期约束:dyn Trait + 'a
当你把一个对象转换成 dyn Trait(动态分发)并存储起来时,编译器需要知道这个“背后的具体对象”能活多久。
为动态对象打上“保质期”标签
如果不加 + 'a,编译器默认 dyn Trait 是 + 'static。这会导致你无法将带有局部引用的对象转为 Trait 对象。
trait Message {
fn print(&self);
}
struct SimpleMessage<'a>(&'a str);
impl<'a> Message for SimpleMessage<'a> {
fn print(&self) { println!("{}", self.0); }
}
// 重点:加上 + 'a,允许这个 Trait 对象持有非静态引用
fn box_message<'a>(msg: SimpleMessage<'a>) -> Box<dyn Message + 'a> {
Box::new(msg)
}
fn main() {
let text = String::from("临时消息");
let msg = SimpleMessage(&text);
let boxed = box_message(msg);
boxed.print();
}
六、特殊生命周期
在 Rust 中,有两个特殊的生命周期符号,一个是代表“永生”的 'static,另一个是代表“由你推导”的 '_。理解它们对于编写高质量的库代码至关重要。
1. 'static 静态生命周期
'static 生命周期代表程序整个运行期间有效的引用,通常用于程序的常量和静态变量:
static NAME: &str = "Rust";
这是 Rust 中最长命的生命周期。但它有两种完全不同的用法,混淆这两者是很多开发者的噩梦。
A. 引用类型:&'static T
这表示引用指向的数据在程序的整个运行期间都存在(通常存储在二进制文件的 .data 或 .rodata 段中)。
// 正确:字符串字面量默认就是 &'static str
let s: &'static str = "我永远存在";
/* 错误演示:试图将局部变量标记为 'static
fn main() {
let local_string = String::from("I am local");
// 报错:local_string 存活时间不够长,无法变为 'static
let s: &'static str = &local_string;
}
*/
B. 特征绑定:T: 'static
这是最容易误解的地方。它不代表 T 必须活一辈子,而是代表:T 内部要么不包含任何引用,要么包含的所有引用都是 'static 的
为什么 String 满足 T: 'static?
String 拥有它自己的数据,它内部不持有任何对外界的引用。所以它被认为是“自给自足”的,满足 T: 'static。
fn print_it<T: 'static>(item: T) {
println!("我接受任何 'static 的数据");
}
fn main() {
let s = String::from("我虽然是局部的,但我没有引用别人");
// ✅ 正确:String 满足 T: 'static
print_it(s);
let local_val = 10;
let r = &local_val;
// ❌ 错误:r 是一个包含局部引用的类型,它不满足 T: 'static
// print_it(r);
}
2. 匿名生命周期占位符 '_
'_ 并不代表一个新的生命周期,它是一个语法糖。它告诉编译器:“这里确实需要一个生命周期,但我不想手动起名字,请按照省略规则帮我填上。”
简化 impl 块中的冗余
当你在为带有生命周期的结构体编写 impl 时,'_ 能让代码清爽很多。
❌ 繁琐写法:到处都是 'a
#![allow(unused)]
fn main() {
struct StrHolder<'a> {
s: &'a str,
}
// 每次都要声明并写出 <'a>
impl<'a> StrHolder<'a> {
fn get_s(&self) -> &str { self.s }
}
}
✅ 优雅写法:使用 '_
// 在 impl 中使用 '_,编译器会自动关联 self 的生命周期
impl StrHolder<'_> {
fn new(s: &str) -> StrHolder<'_> {
StrHolder { s }
}
}
匿名生命周期占位符通常用于简化函数签名,它告诉编译器自动推导生命周期
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap()
}
| 符号 | 它的“潜台词” | 典型场景 |
|---|---|---|
&'static T | “这个数据永远不会死” | 硬编码的配置、静态常量 |
T: 'static | “这个对象内部没有任何临时工” | 需要把对象发给另一个线程 |
'_ | “这里该有个寿命,但我懒得取名,编译器你看着办” | 结构体实现、复杂的嵌套引用省略 |
七、型变
型变是 Rust 编译器处理“子类型转换”的一套规则。在生命周期中,如果 'a: 'b('a 比 'b 长),我们认为 'a 是 'b 的子类型。型变决定了当你把一个“长命引用”传给需要“短命引用”的地方时,这种转换是否合法。
1. 协变(Covariance):长命变短命(向上转型)
协变是指子类型可以作为父类型的替代品。例如,在 Rust 中,&T 是协变的:
#![allow(unused)]
fn main() {
let x: &str = "hello";
let y: &dyn ToString = &x; // 协变
}
这是最自然的逻辑。你有一个活 100 年的引用,传给一个只需要 10 年引用的函数,这显然是安全的。
fn take_short<'b>(input: &'b str) {
println!("我只需要短命引用: {}", input);
}
fn main() {
let s: &'static str = "我是永生的"; // 'static 是所有生命周期的子类型
// ✅ 协变:&'static str 自动转换(降级)为 &'b str
take_short(s);
}
结论:&'a T 对 'a 是协变的,对 T 也是协变的。
2. 逆变(Contravariance): 短命变长命(反向转型)
逆变是指父类型可以作为子类型的替代品,通常在函数类型中出现。
如果 $’a : ’b$,则 $F<’b>$ 可以转换为 $F<’a>$。
// 这里的 F 是 fn(&'a str)
// 如果一个函数能处理短命引用,那么它一定也能处理长命引用。
// 所以 fn(&'short str) 实际上比 fn(&'long str) 的兼容性更强(它是父类)。
// 这是一个“要求高”的函数:它要求传入的处理器必须能处理 'static 引用
fn execute_static_handler(handler: fn(&'static str)) {
handler("I am static");
}
// 这是处理器 A:只能处理 'static
fn static_only(s: &'static str) {
println!("Only static: {}", s);
}
// 这是处理器 B:能力极强,能处理任何生命周期的引用(比如 'a)
fn flexible_handler<'a>(s: &'a str) {
println!("I can handle anything, even short: {}", s);
}
fn main() {
// 1. 传入匹配的处理器,毫无疑问可以
execute_static_handler(static_only);
// 2. 传入“能力更强”的处理器(逆变生效!)
// 虽然 execute_static_handler 要求的是 fn(&'static str)
// 但我们可以把 fn(&'a str) 传给它。
// 因为一个能处理短命引用的函数,必然能安全地处理长命引用。
execute_static_handler(flexible_handler);
}
3. 不变性(Invariance): 严丝合缝(禁止转换)
不变性意味着类型不能进行替换。在 Rust 中,&mut T 是不变的。想象中的错误(如果 &mut 是协变的):
fn swap_ref<'a>(target: &mut &'a str, new_val: &'a str) {
*target = new_val;
}
fn main() {
let mut long_lived: &'static str = "static";
{
let short_lived = String::from("short");
// 如果 &mut 是协变的,这里就会允许把 &mut &'static 降级为 &mut &'local
// 然后 swap_ref 就会把 short_lived 的地址塞进 long_lived 变量
// swap_ref(&mut long_lived, &short_lived);
}
// 大括号结束,short_lived 销毁。
// 如果上面成功了,这里的 long_lived 就指向了垃圾内存!
// println!("{}", long_lived);
}
结论:&mut T 对于 T 是不变的。这种限制强制要求你存入的数据寿命必须与容器定义的寿命绝对相等。
4. 常见容器与智能指针的型变规则
Box<T>、Rc<T>、Cell<T> 等智能指针的型变规则需要特别注意。
在 Rust 的生命周期进阶中,理解常见容器的型变规则(Variance)是避开“生命周期冲突”报错的关键。下表总结了标准库中常用类型的型变特性:
常见类型型变对照表
| 类型 | 对 'a 的型变 | 对 T 的型变 | 备注 |
|---|---|---|---|
&'a T | 协变 | 协变 | 最灵活。长命引用可降级为短命引用。 |
&'a mut T | 协变 | 不变 (Invariant) | 引用本身的寿命可缩短,但指向的内容必须严格匹配。 |
Box<T> | - | 协变 | 拥有所有权,行为类似 T 本身。 |
Vec<T> | - | 协变 | 类似 Box<T>,拥有所有权。 |
**Rc<T> / Arc<T>** | - | 协变 | 虽然共享所有权,但因其不可变性(除非通过内部可变性),保持协变。 |
**Cell<T> / RefCell<T>** | - | 不变 (Invariant) | 内部可变性会导致型变失效,必须严格匹配。 |
fn(T) -> U | - | 逆变 (T), 协变 (U) | 参数是反向的(逆变),返回值是正向的(协变)。 |
A. 为什么 &mut T 对 T 是不变的(Invariant)?
你已经理解了“长命存入短命容器”是危险的。由于 &mut T 允许写入,如果它是协变的,你可能会把一个短命引用塞进一个长命的引用变量里。
fn overwrite<'a>(container: &mut &'a str, new_val: &'a str) {
*container = new_val;
}
fn main() {
let mut static_str: &'static str = "I'm static";
{
let short_string = String::from("short");
// 如果 &mut T 是协变的,这里会把 &mut &'static 降级为 &mut &'a
// 然后 overwrite 会把 short_string 的地址存进 static_str。
// Rust 强制要求 T 必须【完全匹配】,所以这里会报错。
// overwrite(&mut static_str, &short_string);
}
}
B. 为什么 Vec<T> 却是协变的?
这看起来很奇怪:Vec<T> 也能写入,为什么它是协变的?
原因在于所有权(Ownership)。
当你拥有一个 Vec<&'static str> 时,你拥有这个向量的所有权。你可以把它“降级”看作一个 Vec<&'a str>。当你往里面 push 短命引用时,编译器会根据你当前的、降级后的 Vec 类型来检查。
fn main() {
let mut my_vec: Vec<&'static str> = vec!["static"];
// 协变:我们可以把 Vec<&'static str> 传递给接受 Vec<&'a str> 的函数
fn consume_vec<'a>(v: Vec<&'a str>) { /* ... */ }
consume_vec(my_vec); // 成功,发生了协变(所有权转移)
}
关键区别:&mut T 是借用(你是在改别人的东西),而 Vec<T> 是拥有(你在改自己的东西)。
C. 内部可变性的“不变性”陷阱:Cell<T>
Cell<T> 和 RefCell<T> 与 &mut T 一样,允许在不拥有所有权的情况下修改数据。因此,它们必须是不变的。
use std::cell::Cell;
fn main() {
let static_cell: Cell<&'static str> = Cell::new("static");
// 尝试把 Cell<&'static str> 降级为 Cell<&'a str>
// 这在 Rust 中是不允许的(Invariant)
// let cell_alias: &Cell<&str> = &static_cell;
// 如果允许降级,你可以通过 cell_alias 存入一个短命引用,
// 从而破坏原始 static_cell 的静态担保。
}
八、高级特性与复杂模式
1. 高阶特征边界(HRTBs):for<'a>
通常情况下,生命周期参数是由调用者确定的。但有时,我们需要一个函数能够接收一个闭包,而这个闭包处理的引用是在函数内部临时创建的。
无法描述“未来的借用”
如果你使用普通的 <'a>,编译器会认为这个寿命必须在进入函数之前就存在。
// 错误尝试:
/*
fn execute<'a, F>(f: F)
where F: Fn(&'a str)
{
let s = String::from("local");
f(&s); // 报错:s 的寿命不够长。因为 'a 是在调用 execute 前就定死的。
}
*/
// ✅ 正确方案:使用 HRTBs (High-Rank Trait Bounds)
// for<'a> 读作:对于“任何”可能的生命周期 'a
fn execute<F>(f: F)
where F: for<'a> Fn(&'a str)
{
let s = String::from("local");
f(&s); // 成功!f 现在被要求能处理任何寿命的引用,包括函数内部这个临时的
}
2. 闭包中的生命周期推导与显式标注
Rust 闭包的生命周期推导有时会很顽固。特别是当你返回一个借用时,编译器可能无法识别出它应该关联哪个输入。
强制闭包生命周期
在复杂的 Trait 实现中,你可能需要手动给闭包的参数加上“暗示”。
fn main() {
let mut data = vec![1, 2, 3];
// 闭包有时需要显式标注来打破推导僵局
// 这里的标注确保闭包内部的借用不会“逃逸”或错误绑定
let closure = |x: &i32| -> &i32 { x };
let res = closure(&data[0]);
println!("{}", res);
}
3. 再借用(Reborrowing)机制
这是 Rust 默默为你做的一项优化。当你把一个 &mut T 传给函数时,你并没有“移动”这个可变引用,而是进行了一次透明的再借用。
为什么可变引用可以连续使用?
按理说 &mut T 是不具备 Copy 特性的,传给函数后应该就没了。但得益于再借用,你可以这样做:
fn touch(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut message = String::from("Hello");
let r = &mut message;
touch(r); // 这里发生了 reborrow: touch(&mut *r)
touch(r); // r 依然可用!
// 但是:你不能在持有再借用的同时使用原始引用
// let r2 = &mut *r; // r2 是从 r 再借用的
// touch(r); // 错误:此时 r 被 r2 锁定了
// touch(r2);
}
4. 非词法生命周期(NLL, Non-Lexical Lifetimes)
在旧版 Rust 中,生命周期必须持续到大括号结束。NLL 引入后,编译器会分析数据流图,只要一个引用在之后不再被使用,它的寿命就会提前结束。
在同一个块中先借用再修改
这在处理字典(HashMap)的“查询并修改”逻辑时非常有用。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("key", "value");
let val = map.get("key"); // 不可变借用开始
if let Some(v) = val {
println!("{}", v);
} // NLL 识别到 val 的使用到此结束
map.insert("key", "new_value"); // ✅ 成功:即使没出大括号,借用也已提前释放
}
5. 生命周期收缩(Lifetime Narrowing)
这其实是协变的一种表现。当编译器发现你把一个长命引用赋值给一个短命需求时,它会自动“收缩”引用的法律身份。这通常发生在匹配分支或循环中,确保类型系统能通过。