回顾所有权:引用到底能活多久
上一篇我们聊过 Rust 的所有权规则:一个值同一时刻只能有一个所有者,离开作用域就被 drop。但实际写代码时,我们经常把"引用"到处传来传去——这就引出一个问题:这个引用,会不会在它指向的值被释放之后,还被人用到?
Rust 的解决办法是:在编译期就把每个引用的"活多久"算清楚。大多数时候编译器能自己推断,但有些情况它需要你写出来——这就是生命周期标注。
生命周期不是运行时的东西,它只是给编译器看的一段注释,用来检查"这个引用一定比那个引用活得久"。
- 'a生命周期标注的写法,本质是给引用起个名字。
- 省略规则大多数普通函数其实不用写标注。
- trait定义一组方法签名,让不同类型共享行为。
- trait bound用泛型时声明"这个类型必须实现某个 trait"。
生命周期标注:给引用加个名字
下面这段代码编译器会报错:返回的到底是 x 还是 y?它不知道返回的引用该活多久。
// 编译报错:返回的引用没有标注生命周期
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
加上生命周期标注 'a 就清楚了:返回的引用活得和 x、y 中较短的那个一样长。这个 'a 是你给这几个引用起的共同名字。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
注意标注不改变任何运行时行为,只是告诉编译器"它们的生存期是关联的",让借用检查器能放心放行。
函数里的生命周期:三个常见写法
好消息是 Rust 有"生命周期省略规则",三种常见情况其实不用写:
- 每个引用参数自动分到一个独立生命周期;
- 如果只有一个输入引用,它的生命周期会赋给所有输出引用;
- 如果是方法(self 是引用),self 的生命周期赋给输出。
所以 fn first_word(s: &str) -> &str 完全合法,编译器自己推。只有当输入有多个引用、输出又和它们都有关系时,才需要你手写 'a。
结构体里持有引用
如果结构体里想存一个引用(比如借用一段字符串而不是复制它),结构体也得标注生命周期,确保"结构体活多久,它借用的数据就得活多久"。
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("从前有座山。山上有座庙。");
let ex = Excerpt {
part: novel.split('.').next().unwrap(),
};
println!("{}", ex.part); // 从前有座山
}
只要 novel 在 ex 之前不被 drop,这段就安全——借用检查器替你盯着。
trait:给类型定义共同行为
trait 类似其他语言里的"接口":它定义一组方法签名,多个类型可以去实现它。下面定义一个 Summarizable trait,让 NewsArticle 和 Tweet 都实现它。
pub trait Summarizable {
fn summary(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub content: String,
}
impl Summarizable for NewsArticle {
fn summary(&self) -> String {
format!("{} —— {}", self.headline, self.content)
}
}
这样不同类型只要实现同一个 trait,就能被当成同一种东西处理——这就是 Rust 实现"多态"的方式。
泛型 + trait bound:写通用代码
有了 trait,就能写"只要实现了某个 trait,就能用"的泛型函数。trait bound 就是给泛型参数加的一道资格门槛。
fn notify<T: Summarizable>(item: &T) {
println!("现在播报:{}", item.summary());
}
// 也可以用 where 子句,参数多了更清爽
fn pair<T, U>(a: T, b: U)
where
T: Summarizable,
U: Summarizable,
{
println!("{} / {}", a.summary(), b.summary());
}
这种写法既保留了泛型的灵活性,又让编译器能在编译期把它单态化成具体类型——没有运行时虚表的开销,却拿到了接近 C++ 模板的性能。
实战:带引用的简易缓存
把生命周期和 trait 串起来:做一个结构体,它借用一段输入数据,并实现一个"摘要" trait,演示怎么在不拷贝数据的情况下安全地持有引用。
struct Cached<'a, T: Display> {
raw: &'a T,
label: &'a str,
}
impl<'a, T: Display> Cached<'a, T> {
fn show(&self) {
println!("[{}] = {}", self.label, self.raw);
}
}
fn main() {
let data = 3.14159;
let cache = Cached { raw: &data, label: "pi" };
cache.show(); // [pi] = 3.14159
}
整个过程没有堆分配、没有数据拷贝,引用却被生命周期标记得明明白白。这就是 Rust 既能零成本抽象、又能编译期排除一整类内存 bug 的底气。
生命周期与 trait 是 Rust 从"能跑"走向"能复用"的两步:前者保证引用安全,后者让不同类型共享行为。