Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lifetimes: Telling the Compiler How Long References Live | 生命周期:告诉编译器引用能活多久

What you’ll learn: Why lifetimes exist (no GC means the compiler needs proof), lifetime annotation syntax, elision rules, struct lifetimes, the 'static lifetime, and common borrow checker errors with fixes.

你将学到什么: 为什么生命周期会存在(没有 GC 时,编译器必须拿到安全性证明)、生命周期标注语法、 生命周期省略规则、结构体中的生命周期、'static 生命周期,以及常见借用检查器报错及修复方式。

Difficulty: Advanced

难度: 高级

C# developers never think about reference lifetimes - the garbage collector handles reachability. In Rust, the compiler needs proof that every reference is valid for as long as it’s used. Lifetimes are that proof.

C# 开发者通常不会思考“引用的生命周期”这个问题,因为垃圾回收器会负责对象可达性。而在 Rust 中,编译器必须拿到证明,确认每个引用在被使用期间都始终有效。生命周期就是这份证明。

Why Lifetimes Exist | 为什么会有生命周期

#![allow(unused)]
fn main() {
// This won't compile - the compiler can't prove the returned reference is valid
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
// ERROR: missing lifetime specifier - the compiler doesn't know
// whether the return value borrows from `a` or `b`
}
问题不在于 Rust “看不懂”这段代码,而在于它无法证明返回值到底依赖 `a` 还是 `b`,因此没法保证引用一定安全。

Lifetime Annotations | 生命周期标注

// Lifetime 'a says: "the return value lives at least as long as BOTH inputs"
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let result;
    let string1 = String::from("long string");
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("Longest: {result}"); // both references still valid here
    }
    // println!("{result}"); // ERROR: string2 doesn't live long enough
}

C# Comparison | 与 C# 的对比

// C# - the GC keeps objects alive as long as any reference exists
string Longest(string a, string b) => a.Length > b.Length ? a : b;

// No lifetime issues - GC tracks reachability automatically
// But: GC pauses, unpredictable memory usage, no compile-time proof

Lifetime Elision Rules | 生命周期省略规则

Most of the time you don’t need to write lifetime annotations. The compiler applies three rules automatically:

大多数时候你不需要手写生命周期标注。编译器会自动应用三条省略规则:

RuleDescriptionExample
Rule 1Each reference parameter gets its own lifetimefn foo(x: &str, y: &str) -> fn foo<'a, 'b>(x: &'a str, y: &'b str)
规则 1每个引用参数都会获得各自独立的生命周期fn foo(x: &str, y: &str) -> fn foo<'a, 'b>(x: &'a str, y: &'b str)
Rule 2If there’s exactly one input lifetime, it’s assigned to all output lifetimesfn first(s: &str) -> &str -> fn first<'a>(s: &'a str) -> &'a str
规则 2如果只有一个输入生命周期,那么它会被赋给所有输出生命周期fn first(s: &str) -> &str -> fn first<'a>(s: &'a str) -> &'a str
Rule 3If one input is &self or &mut self, that lifetime is assigned to all outputsfn name(&self) -> &str -> works because of &self
规则 3如果某个输入是 &self&mut self,那么该生命周期会被赋给所有输出fn name(&self) -> &str -> 因为有 &self 所以可以自动推断
#![allow(unused)]
fn main() {
// These are equivalent - the compiler adds lifetimes automatically:
fn first_word(s: &str) -> &str { /* ... */ }           // elided
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // explicit

// But this REQUIRES explicit annotation - two inputs, which one does output borrow?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}

Struct Lifetimes | 结构体中的生命周期

// A struct that borrows data (instead of owning it)
struct Excerpt<'a> {
    text: &'a str,  // borrows from some String that must outlive this struct
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn first_sentence(&self) -> &str {
        self.text.split('.').next().unwrap_or(self.text)
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel); // excerpt borrows from novel
    println!("First sentence: {}", excerpt.first_sentence());
    // novel must stay alive as long as excerpt exists
}
// C# equivalent - no lifetime concerns, but no compile-time guarantee either
class Excerpt
{
    public string Text { get; }
    public Excerpt(string text) => Text = text;
    public string FirstSentence() => Text.Split('.')[0];
}
// What if the string is mutated elsewhere? Runtime surprise.
只要结构体里保存的是引用而不是拥有所有权的值,就必须明确告诉编译器:这些引用依赖谁活着。

The 'static Lifetime | 'static 生命周期

#![allow(unused)]
fn main() {
// 'static means "lives for the entire program duration"
let s: &'static str = "I'm a string literal"; // stored in binary, always valid

// Common places you see 'static:
// 1. String literals
// 2. Global constants
// 3. Thread::spawn requires 'static (thread might outlive the caller)
std::thread::spawn(move || {
    // Closures sent to threads must own their data or use 'static references
    println!("{s}"); // OK: &'static str
});

// 'static does NOT mean "immortal" - it means "CAN live forever if needed"
let owned = String::from("hello");
// owned is NOT 'static, but it can be moved into a thread (ownership transfer)
}

Common Borrow Checker Errors and Fixes | 常见借用检查器错误与修复方法

ErrorCauseFix
missing lifetime specifierMultiple input references, ambiguous outputAdd <'a> annotation tying output to correct input
missing lifetime specifier多个输入引用导致输出来源不明确添加 <'a> 之类的标注,把输出与正确输入关联起来
does not live long enoughReference outlives the data it points toExtend the data’s scope, or return owned data instead
does not live long enough引用活得比它指向的数据更久延长原始数据的作用域,或者改为返回拥有所有权的数据
cannot borrow as mutableImmutable borrow still activeUse the immutable reference before mutating, or restructure
cannot borrow as mutable不可变借用仍然处于活动状态先用完不可变借用,再进行修改,或者重构代码
cannot move out of borrowed contentTrying to take ownership of borrowed dataUse .clone(), or restructure to avoid the move
cannot move out of borrowed content试图从借用内容中拿走所有权使用 .clone(),或调整结构避免移动
lifetime may not live long enoughStruct borrow outlives sourceEnsure the source data’s scope encompasses the struct’s usage
lifetime may not live long enough结构体中的借用活得比源数据还久确保源数据的作用域覆盖结构体的使用期

Visualizing Lifetime Scopes | 生命周期作用域可视化

graph TD
    subgraph "Scope Visualization"
        direction TB
        A["fn main()"] --> B["let s1 = String::from(&quot;hello&quot;)"]
        B --> C["{ // inner scope"]
        C --> D["let s2 = String::from(&quot;world&quot;)"]
        D --> E["let r = longest(&s1, &s2)"]
        E --> F["println!(&quot;{r}&quot;)  both alive"]
        F --> G["} // s2 dropped here"]
        G --> H["println!(&quot;{r}&quot;)  s2 gone!"]
    end

    style F fill:#c8e6c9,color:#000
    style H fill:#ffcdd2,color:#000

Multiple Lifetime Parameters | 多个生命周期参数

Sometimes references come from different sources with different lifetimes:

有时候,不同的引用来自不同来源,它们拥有不同的生命周期:

// Two independent lifetimes: the return borrows only from 'a, not 'b
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
    // Return borrows from 'data' only - 'context' can have a shorter lifetime
    data.split(',').next().unwrap_or(data)
}

fn main() {
    let data = String::from("alice,bob,charlie");
    let result;
    {
        let context = String::from("user lookup"); // shorter lifetime
        result = first_with_context(&data, &context);
    } // context dropped - but result borrows from data, not context
    println!("{result}");
}
// C# - no lifetime tracking means you can't express "borrows from A but not B"
string FirstWithContext(string data, string context) => data.Split(',')[0];
// Fine for GC'd languages, but Rust can prove safety without a GC

Real-World Lifetime Patterns | 真实项目中的生命周期模式

Pattern 1: Iterator returning references

模式 1:返回引用的迭代/解析结果

#![allow(unused)]
fn main() {
// A parser that yields borrowed slices from the input
struct CsvRow<'a> {
    fields: Vec<&'a str>,
}

fn parse_csv_line(line: &str) -> CsvRow<'_> {
    // '_ tells the compiler "infer the lifetime from the input"
    CsvRow {
        fields: line.split(',').collect(),
    }
}
}

Pattern 2: “Return owned when in doubt”

模式 2:拿不准时就返回拥有所有权的值

#![allow(unused)]
fn main() {
// When lifetimes get complex, returning owned data is the pragmatic fix
fn format_greeting(first: &str, last: &str) -> String {
    // Returns owned String - no lifetime annotation needed
    format!("Hello, {first} {last}!")
}

// Only borrow when:
// 1. Performance matters (avoiding allocation)
// 2. The relationship between input and output lifetime is clear
}

Pattern 3: Lifetime bounds on generics

模式 3:泛型上的生命周期约束

#![allow(unused)]
fn main() {
// "T must live at least as long as 'a"
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
    cache.push(item);
}

// Common in trait objects: Box<dyn Display + 'a>
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
    Box::new(text)
}
}

When to Reach for 'static | 什么时候该用 'static

ScenarioUse 'static?Alternative
String literalsYes - they’re always 'static-
字符串字面量是 - 它们天然就是 'static-
thread::spawn closureOften - thread outlives callerUse thread::scope for borrowed data
thread::spawn 闭包通常是 - 因为线程可能活得比调用者更久对借用数据可使用 thread::scope
Global configlazy_static! or OnceLockPass references through params
全局配置常见做法是 lazy_static!OnceLock也可以通过参数传引用
Trait objects stored long-termOften - Box<dyn Trait + 'static>Parameterize the container with 'a
长期保存的 trait 对象通常需要 - 如 Box<dyn Trait + 'static>'a 给容器参数化
Temporary borrowingNever - over-constrainingUse the actual lifetime
临时借用不要 - 会把约束写得过死使用真实生命周期即可
Exercise: Lifetime Annotations | 练习:补全生命周期标注 (click to expand / 点击展开)

Challenge: Add the correct lifetime annotations to make this compile:

挑战: 为下面代码补上正确的生命周期标注,使其能够编译:

#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// TODO: Add lifetime annotations
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// TODO: This struct borrows from Config - add lifetime parameter
struct ConnectionInfo {
    db_url: &str,
    api_key: &str,
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// Rule 3 doesn't apply (no &self), Rule 2 applies (one input -> output)
// So the compiler handles this automatically - no annotation needed!
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// Struct lifetime annotation needed:
struct ConnectionInfo<'a> {
    db_url: &'a str,
    api_key: &'a str,
}

fn make_info<'a>(config: &'a Config) -> ConnectionInfo<'a> {
    ConnectionInfo {
        db_url: &config.db_url,
        api_key: &config.api_key,
    }
}
}

Key takeaway: Lifetime elision often saves you from writing annotations on functions, but structs that borrow data always need explicit <'a>.

关键结论: 函数中的生命周期常常能依赖省略规则自动推断,但只要结构体里保存的是借用数据,就必须显式写出 <'a>