所有权、借用和生命周期 intermediate 30 分钟 更新 2026-06-15

生命周期入门

用生命周期标注表达引用之间的存活关系。

生命周期入门

你会学到什么

  • 生命周期标注 '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,含义是:“返回的引用至少和两个入参中存活更短的那个一样久”。调用方据此就能保证:只要 leftright 都还活着,返回值就是安全的。注意示例 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