讲解

生命周期(lifetime)是借用检查器的记账系统:每个引用都有一个「它指向的数据至少活到什么时候」的范围,编译器靠它证明所有引用在使用时必然有效。好消息是绝大多数情况下编译器能自己推出来(生命周期省略规则),你根本不用写;只有当「返回值引用到底借自哪个参数」这类关系有歧义时,才需要手动标注。

标注语法是撇号加小写名:'a。fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 读作:「x、y 和返回值共享同一个生命周期下限 a——返回值的有效期不会超过两个参数中较短的那个」。注意生命周期标注不改变任何值的实际存活时间,它只是向编译器描述引用之间的关系,让编译器有依据批准或拒绝代码。

三条省略规则值得知道(输入引用各占一个生命周期;只有一个输入引用时它赋给所有输出;方法里 &self 的生命周期赋给所有输出)——正因为它们,前面章节写的 calculate_length(s: &String) -> usize 才不用标注。还有一个特殊的 'static:活到程序结束,所有字符串字面量都是 &'static str。常见误解是把 'static 当「万能续命」乱用——它表示「真的活到程序结束」,不是「让编译器闭嘴」。

示例

需要标注的经典场景:返回两个引用中较长的那个:

// 'a 声明:返回值的生命周期不超过 x 和 y 中较短者
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("长长的字符串");
    let s2 = String::from("短");
    let result = longest(&s1, &s2);
    assert_eq!(result, "长长的字符串");

    // 不同作用域也成立:返回值的有效期不超过较短的那个输入
    let s3 = String::from("另一个长字符串");
    let r;
    {
        let s4 = String::from("短");
        r = longest(&s3, &s4);
        println!("{r}");
        assert_eq!(r, "另一个长字符串"); // s4 还活着,r 在这里有效
    }
    // 出了内层作用域 s4 已释放,r 随之失效——再用就是编译错误
}

试图返回局部变量的引用会被直接拒绝(构建脚本验证这段确实编译失败):

// compile-fail: 返回局部变量的引用(悬垂引用)
fn dangle<'a>() -> &'a String {
    let s = String::from("局部字符串");
    &s
}

fn main() {
    let r = dangle();
    println!("{r}");
}

报错大意(E0515,s 是局部变量,函数结束时就会被释放):

error[E0515]: cannot return reference to local variable `s`
 --> main.rs:4:5
  |
4 |     &s
  |     ^^ returns a reference to data owned by the current function

常见坑

  • 以为标注能延长生命:生命周期标注只是「描述关系」,数据该什么时候释放还是什么时候释放;想让数据活久点,改的是所有权不是标注。
  • 动不动 'static:收到「expects 'static」报错时,正确解法通常是改设计(传所有权、用 String 代替 &str),而不是把类型硬掰成 'static。
  • 每个函数都手写标注:先看能不能靠省略规则通过——能推断就不标注,和类型标注的原则一样。
  • 绕开问题用 unsafe:生命周期报错意味着设计上真的有悬垂风险;正确回应是理清数据归谁所有,而不是关检查。

小结

生命周期是引用的有效期证明;多数场景编译器自动省略;手写标注描述「返回值借自谁」;'static 表示活到程序结束。至此 Rust 所有权三件套(所有权、借用、生命周期)全部就位。下一章轻松一下:闭包与迭代器。