生命周期入门
你会学到什么
- 生命周期标注
'a描述引用能存活多久,不改变值实际的生存期。 - 当函数返回引用时,为什么借用检查器需要你显式标注。
- 持有引用的结构体必须带生命周期参数,以及如何为它写方法。
- 生命周期省略规则(elision):多数时候你根本不用写
'a。 'static:贯穿整个程序的生命周期。
最小示例
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() { left } else { right }
}
运行代码
cd examples
cargo run -p rt_09_lifetimes
cargo test -p rt_09_lifetimes
代码讲解
下面对照本页底部自动嵌入的完整源码,逐个概念展开。
为什么 longest 需要 'a
longest 返回一个引用,但它在编译期无法确定返回的到底是 left 还是 right(取决于运行时长度)。借用检查器因此不知道该用哪个入参的存活期来约束返回值。
<'a> 声明一个生命周期参数;把两个入参和返回值都标成 &'a str,含义是:“返回的引用至少和两个入参中存活更短的那个一样久”。调用方据此就能保证:只要 left、right 都还活着,返回值就是安全的。注意示例 main 里我们故意让两个入参来自不同作用域,说明它们的生存期不必相同。
持有引用的结构体与方法
结构体若存引用,就必须带生命周期参数:
struct Excerpt<'a> {
part: &'a str,
}
这表示 Excerpt 实例不能比它引用的字符串活得更久。impl<'a> Excerpt<'a> 里我们写了构造函数 new 和两个方法,演示如何在带生命周期的类型上实现行为。
生命周期省略规则
并非所有引用都要手写 'a。编译器内置了几条省略规则:
- 每个引用参数各得一个独立的生命周期;
- 若只有一个输入引用,输出引用就用它的生命周期;
- 若参数里有
&self(方法),输出引用一律跟随&self。
所以示例里的 first_word(s: &str) -> &str 和方法 part(&self) -> &str 都不用写 'a——这也正是前几章我们从没见过生命周期标注的原因。announce_and_return_part 即使多带一个 &str 参数,因为有 &self,返回值依旧无需标注。
'static
'static 表示引用在整个程序运行期间都有效。字符串字面量天生是 &'static str(被编进二进制、永不释放),所以 motto() 能安全地把它返回出来。
常见错误
返回一个指向函数内部局部变量的引用:
fn dangling() -> &String {
let s = String::from("temp");
&s // ❌ s 在函数结束时被释放
}
修复方式是返回所有权(String)而不是引用。
练习
- 给
Excerpt加一个方法len,返回part的字节长度(试试它需不需要写'a)。 - 写一个函数
first<'a>(items: &'a [String]) -> &'a str返回第一个元素的切片,并思考它能否靠省略规则去掉'a。 - 把
motto的返回类型从&'static str改成&str,看看编译器报什么错,体会'static的含义。
小结
生命周期是借用检查器的“时间维度”,让返回引用和存引用的结构体都安全。多数时候靠省略规则自动推断。
下一步
掌握所有权后,进入用 struct 和 enum 给真实问题建模的阶段。
完整示例代码
下面是 examples/09_lifetimes/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/09_lifetimes/src/main.rs
//! 生命周期(lifetime):告诉编译器“引用之间的存活关系”。
//!
//! 生命周期标注**不会**改变任何值实际存活多久,它只是给借用检查器
//! (borrow checker)提供信息,让它能在编译期证明“引用永远不会指向
//! 已经被释放的数据”(即避免悬垂引用 dangling reference)。
//!
//! 本示例覆盖:
//! 1. 函数上的显式生命周期标注(`longest`),以及为什么借用检查器需要它;
//! 2. 持有引用的结构体(`Excerpt<'a>`)及其方法;
//! 3. 生命周期省略规则(elision)——为什么 `first_word` 不用写 `'a`;
//! 4. `'static` 生命周期。
/// 返回两个字符串切片中**较长**的那一个。
///
/// 为什么必须写 `<'a>`?因为函数返回一个引用,编译器无法自己判断
/// 这个引用到底借自 `left` 还是 `right`(运行时才知道)。于是我们用
/// 同一个生命周期 `'a` 标注两个入参和返回值,含义是:
/// “返回的引用至少和两个入参中**存活更短**的那个一样久”。
///
/// 调用方因此知道:只要 `left` 和 `right` 都还活着,返回值就是安全的。
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() {
left
} else {
right
}
}
/// 一个“书摘”:它**借用**了别处字符串的一部分,并不拥有数据。
///
/// 因为字段 `part` 是引用,结构体必须声明生命周期参数 `'a`。
/// 它表达的约束是:`Excerpt` 实例**不能比**它所引用的字符串活得更久,
/// 否则 `part` 就会变成悬垂引用。
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
/// 关联函数:从一段文本切片构造书摘。
fn new(part: &'a str) -> Self {
Self { part }
}
/// 返回内部引用。
///
/// 这里**用到了生命周期省略规则**:方法只有一个 `&self` 输入引用,
/// 编译器自动把返回值的生命周期设为和 `&self` 相同,所以我们
/// 无需手写 `-> &'a str`。
fn part(&self) -> &str {
self.part
}
/// 打印一句“通知”,并返回 `self.part`。
///
/// 当输入里有 `&self` 时,省略规则规定:返回引用的生命周期跟随 `&self`,
/// 因此即使有多个引用参数(`announcement`),这里依然不必标注。
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!(" 注意请看:{announcement}");
self.part
}
}
/// 返回第一个单词(到第一个空格为止)。
///
/// 这是**生命周期省略**的经典例子:只有一个输入引用 `s`,编译器自动
/// 把返回引用的生命周期绑定到 `s`。等价于手写 `fn first_word<'a>(s: &'a str) -> &'a str`,
/// 但我们什么都不用写。
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
/// 返回一个 `'static` 引用:它指向程序的整个生命周期都存在的数据。
///
/// 字符串字面量(`&'static str`)被直接编入二进制,永远不会被释放,
/// 因此可以安全地从任意位置返回。
fn motto() -> &'static str {
"安全源于编译期的证明"
}
fn main() {
// 1. 函数上的显式生命周期
let a = String::from("a fairly long string");
let b = String::from("short");
println!("[longest] 较长的是:{}", longest(&a, &b));
// 生命周期允许两个入参来自**不同**作用域,只要在调用点都还活着。
{
let inner = String::from("x");
println!("[longest] 跨作用域:{}", longest(&a, &inner));
}
// 2. 持有引用的结构体 + 方法
let novel = String::from("Call me Ishmael. Some years ago I went sailing.");
let first_sentence = novel.split('.').next().unwrap_or("");
let excerpt = Excerpt::new(first_sentence);
println!("[Excerpt] 首句:{}", excerpt.part());
let part = excerpt.announce_and_return_part("下面是引用的片段");
println!("[Excerpt] 返回的片段:{part}");
// 3. 省略规则:first_word 没有任何 'a 标注,照样工作
let sentence = String::from("hello rust world");
println!("[first_word] 第一个单词:{}", first_word(&sentence));
// 4. 'static 生命周期
println!("[static] 座右铭:{}", motto());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn picks_longest() {
assert_eq!(longest("abcd", "ab"), "abcd");
assert_eq!(longest("a", "bcd"), "bcd");
}
#[test]
fn longest_prefers_left_on_tie() {
// 长度相等时返回 left(实现里用了 `>=`)。
assert_eq!(longest("ab", "cd"), "ab");
}
#[test]
fn excerpt_borrows() {
let text = String::from("first. second.");
let excerpt = Excerpt::new(&text[..5]);
assert_eq!(excerpt.part(), "first");
}
#[test]
fn announce_returns_part() {
let excerpt = Excerpt::new("hello");
assert_eq!(excerpt.announce_and_return_part("看这里"), "hello");
}
#[test]
fn first_word_elision_works() {
assert_eq!(first_word("foo bar"), "foo");
assert_eq!(first_word("single"), "single");
}
#[test]
fn motto_is_static() {
let s: &'static str = motto();
assert!(!s.is_empty());
}
} examples/09_lifetimes/Cargo.toml
[package]
name = "rt_09_lifetimes"
version.workspace = true
edition.workspace = true
publish.workspace = true