☰
Rust By Example 精讲:理解 Rust 生命周期(Lifetime)——从借用检查到 ‘static 的完整指南
2026/10/6 1:49:27 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】rust-by-example

Learn Rust with examples (Live code editor included)

项目地址:https://gitcode.com/gh_mirrors/ru/rust-by-example
点击查看免费下载

生命周期(lifetime)是 Rust 所有权系统中保障所有借用(borrow)合法性的核心机制,由编译器中的**借用检查器(borrow checker)**负责验证。本指南基于开源仓库 rust-by-example 中src/scope/lifetime.md及src/scope/lifetime/目录下全部子文档,系统讲解生命周期的本质、与作用域的区别、显式标注语法、函数与方法中的生命周期、结构体与 trait 中的应用、生命周期边界、协变(coercion)以及'static的两种常见含义。阅读完本文,你将能够准确理解借用检查器的底层约束逻辑,熟练编写带生命周期标注的泛型代码,并能正确区分'static引用与'statictrait 边界。

生命周期与作用域:两个经常被混淆的概念

严格来说,lifetime(生命周期)是编译器(更具体地说是它的borrow checker)用来确保所有借用都合法的一种构造。变量的生命周期从它被创建时开始,到它被销毁时结束。虽然生命周期和作用域(scope)经常被放在一起谈论,但它们并不是同一回事。

以一个通过&借用变量的场景为例:

  • 借用的生命周期由它被声明的位置决定,因此只要借用在其出借方(lender)被销毁之前结束,这个借用就是合法的;
  • 借用的作用域则是由该引用被使用的位置决定的。

下面的示例用注释线条直观地标注了每个变量的创建与销毁时刻,展示了生命周期与作用域的关系:

// 生命周期在下方用线条标注:每行表示一个变量的创建与销毁。 // `i` 的生命周期最长,因为它的作用域完全包住了 `borrow1` 和 `borrow2`。 // `borrow1` 与 `borrow2` 的持续时间相比是无关紧要的,因为两者互不相交。 fn main() { let i = 3; // `i` 的生命周期开始。────────────────┐ // │ { // │ let borrow1 = &i; // `borrow1` 生命周期开始。──┐│ // ││ println!("borrow1: {}", borrow1); // ││ } // `borrow1` 生命周期结束。──────────────────────┘│ // │ { // │ let borrow2 = &i; // `borrow2` 生命周期开始。──┐│ // ││ println!("borrow2: {}", borrow2); // ││ } // `borrow2` 生命周期结束。──────────────────────┘│ // │ } // `i` 的生命周期结束。─────────────────────────────┘

需要注意,在这个阶段,生命周期没有名称也没有类型,这限制了我们后续可以如何使用它们——真正给生命周期“起名字”是下一节显式标注(explicit annotation)的工作。

显式标注:生命周期参数的基础语法

借用检查器使用显式生命周期标注来确定引用应当有效多久。在生命周期无法被省略(elision)的情况下,Rust 要求我们显式标注,以确定引用的生命周期。显式标注的语法使用撇号(apostrophe,')字符:

foo<'a> // `foo` 有一个生命周期参数 `'a`

与闭包类似,使用生命周期需要引入泛型。该语法同时表明:foo的生命周期不能超过'a。显式标注一个类型的生命周期形式为&'a T,其中'a已经被引入。

当存在多个生命周期时,语法类似:

foo<'a, 'b> // `foo` 拥有生命周期参数 `'a` 和 `'b`

此时foo的生命周期既不能超过'a,也不能超过'b(即取两者中较短者)。

看一个显式生命周期标注的完整示例:

// `print_refs` 接收两个具有不同生命周期 `'a` 和 `'b` 的 `i32` 引用。 // 这两个生命周期的长度都必须至少与函数 `print_refs` 一样长。 fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) { println!("x is {} and y is {}", x, y); } // 一个不接受任何参数、但带有生命周期参数 `'a` 的函数。 fn failed_borrow<'a>() { let _x = 12; // 错误:`_x` 存活时间不够长 let _y: &'a i32 = &_x; // 尝试在函数内部把生命周期 `'a` 用作显式类型标注会失败, // 因为 `&_x` 的生命周期比 `_y` 的短。 // 短的生命周期不能被强制转换(coerce)成更长的生命周期。 } fn main() { // 创建下面要被借用的变量。 let (four, nine) = (4, 9); // 把两个变量的借用(`&`)传入函数。 print_refs(&four, &nine); // 任何被借用的输入都必须比借用者活得更久。 // 换句话说,`four` 和 `nine` 的生命周期必须比 `print_refs` 更长。 failed_borrow(); // `failed_borrow` 中没有引用去迫使 `'a` 比函数本身的生命周期更长, // 但由于 `'a` 更"长",且生命周期从未被约束,它默认退化为 `'static`。 }

这里failed_borrow的失败原因值得展开:'a是一个由调用者决定的、可能任意长的生命周期,而局部变量_x的生命周期在函数返回时必然结束。把一个只活到函数末尾的引用赋给一个"可能活到程序末尾"的类型&'a i32,借用检查器会直接拒绝——短生命周期永远不能被强制变长。

提示:文档中省略(elision)隐式标注生命周期,这与显式标注是两种不同的机制,详见 生命周期省略规则。

省略规则(Elision):编译器替我们补齐常见模式

有些生命周期模式极其常见,因此借用检查器允许我们省略它们以节省输入并提高可读性,这被称为elision(省略)。Elision 在 Rust 中存在的唯一原因就是这些模式过于普遍。下面这段代码展示了几个省略的例子(完整的 elision 规则描述可参考官方书籍中的lifetime elision章节,仓库内的 elision.md 提供了精炼版):

// `elided_input` 与 `annotated_input` 的签名本质上完全相同, // 因为 `elided_input` 的生命周期由编译器推断得出: fn elided_input(x: &i32) { println!("`elided_input`: {}", x); } fn annotated_input<'a>(x: &'a i32) { println!("`annotated_input`: {}", x); } // 同样,`elided_pass` 与 `annotated_pass` 的签名相同, // 因为生命周期被隐式地添加到 `elided_pass` 上: fn elided_pass(x: &i32) -> &i32 { x } fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x } fn main() { let x = 3; elided_input(&x); annotated_input(&x); println!("`elided_pass`: {}", elided_pass(&x)); println!("`annotated_pass`: {}", annotated_pass(&x)); }

这里传递出的核心信息是:elision 只是语法糖。fn elided_input(x: &i32)在编译器看来与fn annotated_input<'a>(x: &'a i32)完全等价,单输入引用参数的省略规则自动为其补上生命周期。同理,fn elided_pass(x: &i32) -> &i32会被推断为返回参数的生命周期。省略并没有改变借用检查的语义,只是减少了书写负担。

函数中的生命周期:三条硬性约束

忽略省略规则之后,带生命周期的函数签名有几个约束:

  • 任何引用都必须标注生命周期;
  • 任何被返回的引用,必须与某个输入拥有相同的生命周期,或者是'static;
  • 另外注意,如果"无输入却返回引用"会导致返回对无效数据的引用,这种写法是禁止的。

下面的例子展示了带生命周期函数的一些合法形态:

// 一个带生命周期 `'a` 的输入引用,`'a` 必须至少与函数活得一样久。 fn print_one<'a>(x: &'a i32) { println!("`print_one`: x is {}", x); } // 可变引用同样可以使用生命周期。 fn add_one<'a>(x: &'a mut i32) { *x += 1; } // 多个元素可以拥有不同生命周期。本例中两者用同一个 `'a` 也可以, // 但在更复杂的场景下,可能需要不同的生命周期。 fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) { println!("`print_multi`: x is {}, y is {}", x, y); } // 返回传入的引用是允许的,但必须返回正确的生命周期。 fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x } //fn invalid_output<'a>() -> &'a String { &String::from("foo") } // 上面的写法是非法的:`'a` 必须活得比函数更长。 // 这里 `&String::from("foo")` 会先创建一个 `String`,再取其引用。 // 随后该数据在离开作用域时被丢弃,导致返回的引用指向无效数据。 fn main() { let x = 7; let y = 9; print_one(&x); print_multi(&x, &y); let z = pass_x(&x, &y); print_one(z); let mut t = 3; add_one(&mut t); print_one(&t); }

注意pass_x的精妙之处:它同时接收'a和'b两个生命周期参数,但返回类型只标注&'a i32,说明返回值与第二个参数y的生命周期无关。而invalid_output这类"凭空返回引用"的函数,要么显式标注'static并返回程序级的静态数据,要么直接被借用检查器拒绝。

方法中的生命周期:与函数相同的标注方式

方法(method)的生命周期标注与独立函数类似,在impl块内为&self或&mut self等引用参数标注生命周期即可:

struct Owner(i32); impl Owner { // 像标注独立函数一样标注生命周期。 fn add_one<'a>(&'a mut self) { self.0 += 1; } fn print<'a>(&'a self) { println!("`print`: {}", self.0); } } fn main() { let mut owner = Owner(18); owner.add_one(); owner.print(); }

这里&'a mut self与&'a self的意义在于约束可变借用与不可变借用的作用范围。在更复杂的方法中(例如返回self内部字段的引用),这种显式标注是让借用检查器理解"返回值与&self同生命周期"的关键。

结构体与枚举中的生命周期:引用成员必须活得比容器久

结构体中的生命周期标注也与函数类似,其核心约束是:结构体持有的引用,其生命周期必须比结构体本身更长。结构体、枚举同样适用这一规则:

// 类型 `Borrowed` 持有一个 `i32` 的引用。 // 该 `i32` 的引用必须活得比 `Borrowed` 更久。 #[derive(Debug)] struct Borrowed<'a>(&'a i32); // 同理,这里两个引用都必须活得比结构体更久。 #[derive(Debug)] struct NamedBorrowed<'a> { x: &'a i32, y: &'a i32, } // 一个枚举,要么是 `i32`,要么是它的引用。 #[derive(Debug)] enum Either<'a> { Num(i32), Ref(&'a i32), } fn main() { let x = 18; let y = 15; let single = Borrowed(&x); let double = NamedBorrowed { x: &x, y: &y }; let reference = Either::Ref(&x); let number = Either::Num(y); println!("x is borrowed in {:?}", single); println!("x and y are borrowed in {:?}", double); println!("x is borrowed in {:?}", reference); println!("y is *not* borrowed in {:?}", number); }

注意Either枚举体现的细节:Ref(&'a i32)携带引用,而Num(i32)完全拥有数据、不需要生命周期参数与之关联。这就是为什么我们常用Borrowed这类包装类型来设计"视图"型数据结构——生命周期参数让类型系统能够精确追踪其中每个引用与外部数据的存活关系。

trait 中的生命周期:impl 也需要标注

trait 方法中的生命周期标注与函数基本类似,并且impl块本身也可以标注生命周期。看仓库中的 trait.md 示例:

// 一个带生命周期标注的结构体。 #[derive(Debug)] struct Borrowed<'a> { x: &'a i32, } // 为 impl 标注生命周期。 impl<'a> Default for Borrowed<'a> { fn default() -> Self { Self { x: &10, } } } fn main() { let b: Borrowed = Default::default(); println!("b is {:?}", b); }

impl<'a> Default for Borrowed<'a>表明:为所有生命周期'a的Borrowed<'a>实现Defaulttrait。Default::default()在这里返回一个持有&10(即&'static i32)的实例——'static引用可以被强制缩短为任意生命周期,因此这个实现对于任何'a都成立。

生命周期边界(Bounds):T: 'a与T: Trait + 'a

正如泛型类型可以有边界,生命周期(本身也是泛型)同样使用边界。这里的:字符含义略有不同,但+的用法相同。请注意下面的两种读法:

  1. T: 'a:T中的所有引用都必须活得比生命周期'a更久;
  2. T: Trait + 'a:类型T必须实现 traitTrait,且T中的所有引用都必须活得比'a更久。

下面这个例子展示了上述语法在where关键字之后的使用:

use std::fmt::Debug; // 用于约束的 trait。 #[derive(Debug)] struct Ref<'a, T: 'a>(&'a T); // `Ref` 持有一个对泛型类型 `T` 的引用,`T` 带有某个 `Ref` 未知的生命周期 `'a`。 // `T` 被约束为:`T` 中的任何 *引用* 都必须活得比 `'a` 更久。 // 另外,`Ref` 自身的生命周期不能超过 `'a`。 // 一个使用 `Debug` trait 打印的泛型函数。 fn print<T>(t: T) where T: Debug { println!("`print`: t is {:?}", t); } // 这里接收对 `T` 的引用,其中 `T` 实现了 `Debug`, // 且 `T` 中的所有 *引用* 都活得比 `'a` 更久。 // 此外,`'a` 必须活得比函数更久。 fn print_ref<'a, T>(t: &'a T) where T: Debug + 'a { println!("`print_ref`: t is {:?}", t); } fn main() { let x = 7; let ref_x = Ref(&x); print_ref(&ref_x); print(ref_x); }

这里的T: 'a边界解决了泛型编程中的常见难题:当泛型参数T内部可能携带引用时,类型系统无法自动得知这些引用能存活多久。T: 'a显式声明"T中所有引用都必须活得比'a久",从而保证Ref<'a, T>持有一个在'a内始终有效的&'a T。相关概念可进一步参考仓库中的 generics.md、generics/bounds.md 与 generics/multi_bounds.md。

生命周期协变(Coercion):长生命周期可以被"缩短"

更长的生命周期可以被强制转换为更短的生命周期(lifetime coercion),从而使其能在一个原本不允许的作用域内工作。这种协变既以 Rust 编译器推断的形式存在,也可以显式声明生命周期之间的差异:

// 这里,Rust 推断出一个尽可能短的生命周期。 // 两个引用随后被强制转换到该生命周期。 fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 { first * second } // `<'a: 'b, 'b>` 读作:生命周期 `'a` 至少与 `'b` 一样长。 // 这里我们接收一个 `&'a i32`,通过协变返回一个 `&'b i32`。 fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 { first } fn main() { let first = 2; // 更长的生命周期 { let second = 3; // 更短的生命周期 println!("The product is {}", multiply(&first, &second)); println!("{} is the first", choose_first(&first, &second)); }; }

这里有两个关键机制:

  • 推断协变:multiply中两个参数被标注为同一个'a,编译器自动选择"两者中较短者"作为实际生命周期,因此把&first(长)强制缩短到&second(短)的范围内;
  • 显式子类型关系:'a: 'b语法明确声明'a至少与'b一样长,允许choose_first把'a引用"降级"为'b引用返回。

'static 生命周期:两种相关但截然不同的含义

Rust 有几个保留的生命周期名称,其中之一就是'static。你会在两种场景中遇到它:

// 一个具有 'static 生命周期的引用: let s: &'static str = "hello world"; // 'static 作为 trait 边界的一部分: fn generic<T>(x: T) where T: 'static {}

两者相关但含义微妙地不同,这也是 Rust 学习者常见的困惑来源。

作为引用生命周期

作为引用生命周期,'static表示引用所指向的数据在程序的剩余生命周期内都有效。它仍然可以被强制转换为更短的生命周期。有两种常见的产生'static变量的方式,且两者都存放在二进制的只读内存中:

  • 使用static声明创建常量;
  • 创建字符串字面量(其类型为&'static str)。
// 创建一个具有 `'static` 生命周期的常量。 static NUM: i32 = 18; // 返回对 `NUM` 的引用,其 `'static` 生命周期被强制转换为输入参数的生命周期。 fn coerce_static<'a>(_: &'a i32) -> &'a i32 { &NUM } fn main() { { // 创建一个 `string` 字面量并打印它: let static_string = "I'm in read-only memory"; println!("static_string: {}", static_string); // 当 `static_string` 离开作用域时,该引用不再可用, // 但数据本身仍保留在二进制文件中。 } { // 创建一个整数用于 `coerce_static`: let lifetime_num = 9; // 将 `NUM` 强制转换为 `lifetime_num` 的生命周期: let coerced_static = coerce_static(&lifetime_num); println!("coerced_static: {}", coerced_static); } println!("NUM: {} stays accessible!", NUM); }

由于'static引用只需要在程序生命的剩余部分有效,它们也可以在程序运行期间被动态创建。下面的示例使用Box::leak动态创建'static引用——这种情况下它并非存活整个程序,而是从泄漏(leak)时刻开始一直存活:

extern crate rand; use rand::Fill; fn random_vec() -> &'static [u64; 100] { let mut rng = rand::rng(); let mut boxed = Box::new([0; 100]); boxed.fill(&mut rng); Box::leak(boxed) } fn main() { let first: &'static [u64; 100] = random_vec(); let second: &'static [u64; 100] = random_vec(); assert_ne!(first, second) }

作为 trait 边界

作为 trait 边界,'static意味着该类型不包含任何非'static的引用。例如:接收方可以无限期地持有该类型,直到显式丢弃它之前它都不会失效。

理解这一点很重要:任何拥有型数据(owned data)总能通过'static边界,但对这份拥有型数据的引用通常不行:

use std::fmt::Debug; fn print_it(input: impl Debug + 'static) { println!("'static value passed in is: {:?}", input); } fn main() { // i 是拥有型数据且不含引用,因此它是 'static: let i = 5; print_it(i); // 糟糕,&i 只有 main() 作用域定义的生命周期,因此它不是 'static: print_it(&i); }

编译器会给出如下错误:

error[E0597]: `i` does not live long enough --> src/lib.rs:15:15 | 15 | print_it(&i); | ---------^^-- | | | | | borrowed value does not live long enough | argument requires that `i` is borrowed for `'static` 16 | } | - `i` dropped here while still borrowed

这个错误的本质是:print_it要求其参数不含任何非静态引用(从而可以无限期持有),而&i的生命周期局限于main函数作用域,一旦main结束、i被销毁,引用就会悬垂。这正是异步运行时、线程发送(Send/'static绑定)等场景中频繁出现E0597错误的典型原因。

延伸阅读与本章脉络

本章(src/scope/)从 raii.md、borrow.md 与 move.md 逐步推进到生命周期,属于 Rust 所有权体系的中高级主题。建议按照以下顺序深入:

  • 生命周期省略规则 elision.md —— 省略规则的代码演示;
  • 显式标注 explicit.md ——foo<'a>与&'a T语法细节;
  • 函数 fn.md —— 函数签名的生命周期约束;
  • 方法 methods.md ——impl块中的生命周期;
  • 结构体 struct.md —— 结构体与枚举持有的引用;
  • trait.md ——impl与 trait 方法中的生命周期;
  • 生命周期边界 lifetime_bounds.md ——T: 'a与T: Trait + 'a;
  • 生命周期协变 lifetime_coercion.md ——'a: 'b子类型关系;
  • 静态生命周期 static_lifetime.md ——'static引用的两种产生方式与Box::leak。

此外,生命周期与泛型、trait 系统紧密交织,可对照 generics.md 与 trait.md 阅读;关于'static常量可参见 custom_types/constants.md。这些文档与本章共同构成了 rust-by-example 中关于 Rust 所有权与借用体系的完整学习路径。

  • 文档
  • 教程

【免费下载链接】rust-by-example

Learn Rust with examples (Live code editor included)

项目地址:https://gitcode.com/gh_mirrors/ru/rust-by-example
点击查看免费下载
上一篇:Webp2jpg-Online:让图片格式转换变得简单安全的本地化解决方案
下一篇:probe-image-size 项目常见问题解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询