Rust By Example 精讲:理解 Rust 生命周期(Lifetime)——从借用检查到 ‘static 的完整指南 文档教程【免费下载链接】rust-by-exampleLearn 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字符fooa // foo 有一个生命周期参数 a与闭包类似使用生命周期需要引入泛型。该语法同时表明foo的生命周期不能超过a。显式标注一个类型的生命周期形式为a T其中a已经被引入。当存在多个生命周期时语法类似fooa, b // foo 拥有生命周期参数 a 和 b此时foo的生命周期既不能超过a也不能超过b即取两者中较短者。看一个显式生命周期标注的完整示例// print_refs 接收两个具有不同生命周期 a 和 b 的 i32 引用。 // 这两个生命周期的长度都必须至少与函数 print_refs 一样长。 fn print_refsa, b(x: a i32, y: b i32) { println!(x is {} and y is {}, x, y); } // 一个不接受任何参数、但带有生命周期参数 a 的函数。 fn failed_borrowa() { 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_inputa(x: a i32) { println!(annotated_input: {}, x); } // 同样elided_pass 与 annotated_pass 的签名相同 // 因为生命周期被隐式地添加到 elided_pass 上 fn elided_pass(x: i32) - i32 { x } fn annotated_passa(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_inputa(x: a i32)完全等价单输入引用参数的省略规则自动为其补上生命周期。同理fn elided_pass(x: i32) - i32会被推断为返回参数的生命周期。省略并没有改变借用检查的语义只是减少了书写负担。函数中的生命周期三条硬性约束忽略省略规则之后带生命周期的函数签名有几个约束任何引用都必须标注生命周期任何被返回的引用必须与某个输入拥有相同的生命周期或者是static另外注意如果无输入却返回引用会导致返回对无效数据的引用这种写法是禁止的。下面的例子展示了带生命周期函数的一些合法形态// 一个带生命周期 a 的输入引用a 必须至少与函数活得一样久。 fn print_onea(x: a i32) { println!(print_one: x is {}, x); } // 可变引用同样可以使用生命周期。 fn add_onea(x: a mut i32) { *x 1; } // 多个元素可以拥有不同生命周期。本例中两者用同一个 a 也可以 // 但在更复杂的场景下可能需要不同的生命周期。 fn print_multia, b(x: a i32, y: b i32) { println!(print_multi: x is {}, y is {}, x, y); } // 返回传入的引用是允许的但必须返回正确的生命周期。 fn pass_xa, b(x: a i32, _: b i32) - a i32 { x } //fn invalid_outputa() - 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_onea(a mut self) { self.0 1; } fn printa(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 Borroweda(a i32); // 同理这里两个引用都必须活得比结构体更久。 #[derive(Debug)] struct NamedBorroweda { x: a i32, y: a i32, } // 一个枚举要么是 i32要么是它的引用。 #[derive(Debug)] enum Eithera { 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 Borroweda { x: a i32, } // 为 impl 标注生命周期。 impla Default for Borroweda { fn default() - Self { Self { x: 10, } } } fn main() { let b: Borrowed Default::default(); println!(b is {:?}, b); }impla Default for Borroweda表明为所有生命周期a的Borroweda实现Defaulttrait。Default::default()在这里返回一个持有10即static i32的实例——static引用可以被强制缩短为任意生命周期因此这个实现对于任何a都成立。生命周期边界BoundsT: a与T: Trait a正如泛型类型可以有边界生命周期本身也是泛型同样使用边界。这里的:字符含义略有不同但的用法相同。请注意下面的两种读法T: aT中的所有引用都必须活得比生命周期a更久T: Trait a类型T必须实现 traitTrait且T中的所有引用都必须活得比a更久。下面这个例子展示了上述语法在where关键字之后的使用use std::fmt::Debug; // 用于约束的 trait。 #[derive(Debug)] struct Refa, T: a(a T); // Ref 持有一个对泛型类型 T 的引用T 带有某个 Ref 未知的生命周期 a。 // T 被约束为T 中的任何 *引用* 都必须活得比 a 更久。 // 另外Ref 自身的生命周期不能超过 a。 // 一个使用 Debug trait 打印的泛型函数。 fn printT(t: T) where T: Debug { println!(print: t is {:?}, t); } // 这里接收对 T 的引用其中 T 实现了 Debug // 且 T 中的所有 *引用* 都活得比 a 更久。 // 此外a 必须活得比函数更久。 fn print_refa, 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久从而保证Refa, T持有一个在a内始终有效的a T。相关概念可进一步参考仓库中的 generics.md、generics/bounds.md 与 generics/multi_bounds.md。生命周期协变Coercion长生命周期可以被缩短更长的生命周期可以被强制转换为更短的生命周期lifetime coercion从而使其能在一个原本不允许的作用域内工作。这种协变既以 Rust 编译器推断的形式存在也可以显式声明生命周期之间的差异// 这里Rust 推断出一个尽可能短的生命周期。 // 两个引用随后被强制转换到该生命周期。 fn multiplya(first: a i32, second: a i32) - i32 { first * second } // a: b, b 读作生命周期 a 至少与 b 一样长。 // 这里我们接收一个 a i32通过协变返回一个 b i32。 fn choose_firsta: 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 genericT(x: T) where T: static {}两者相关但含义微妙地不同这也是 Rust 学习者常见的困惑来源。作为引用生命周期作为引用生命周期static表示引用所指向的数据在程序的剩余生命周期内都有效。它仍然可以被强制转换为更短的生命周期。有两种常见的产生static变量的方式且两者都存放在二进制的只读内存中使用static声明创建常量创建字符串字面量其类型为static str。// 创建一个具有 static 生命周期的常量。 static NUM: i32 18; // 返回对 NUM 的引用其 static 生命周期被强制转换为输入参数的生命周期。 fn coerce_statica(_: a i32) - a i32 { NUM } fn main() { { // 创建一个 string 字面量并打印它 let static_string Im 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 ——fooa与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-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐Rust 生命周期Lifetimes完全指南从借用检查器到 static 的实战解析Rust 生命周期Lifetimes完全指南从借用检查器到 static 的实战解析 导读 生命周期Lifetimes是 Rust 中一类特殊的泛型教程文档Rust 生命周期Lifetime基础实战从借用范围到 Elision 消除规则Rust By Practice 篇Rust 生命周期Lifetime基础实战从借用范围到 Elision 消除规则Rust By Practice 篇 生命周期Lifetime是文档教程示例工程Rust By Practice 精讲彻底理解 static 生命周期与 T: static 特征约束Rust By Practice 精讲彻底理解 static 生命周期与 T: static 特征约束 导读 static 是 Rust 中最特殊、也最文档教程示例工程上一篇Webp2jpg-Online让图片格式转换变得简单安全的本地化解决方案下一篇probe-image-size 项目常见问题解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考