Rust 借用与函数生命周期:从 `simple-borrows` 到函数签名级借用流分析
Rust 借用与函数生命周期从simple-borrows到函数签名级借用流分析【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust在 Rust 中借用检查器borrow checker不仅需要理解变量在单个作用域内的借用关系还必须精确推断借用如何流入和流出函数。本篇基于 Google Android 团队 Rust 课程comprehensive-rustlifetimes 章节展开系统讲解函数调用边界上借用生命周期lifetime的完整模型从最简单的“借用仅持续到函数调用结束”到返回引用、多参数借用消解、生命周期标注与省略规则再到数据结构的生命周期注解。读完你将掌握 Rust 中“借用以函数签名为契约”的核心思维能够读懂并写出正确的生命周期标注代码并理解multiple-borrows、borrow-one、borrow-both等典型场景的编译错误与修复方式。一、函数调用边界上的借用从simple-borrows说起Rust 的借用检查器在分析程序时需要回答一个关键问题一个借用borrow的生命周期何时开始、何时结束。当借用发生在单个表达式内部时很容易判断但一旦涉及函数调用编译器就必须推断借用如何穿过函数边界。lifetimes/simple-borrows.md 给出了这一模型中最简单的情形借用只持续到函数调用结束。课程原文的示例代码完整如下fn borrows(x: i32) { dbg!(x); } fn main() { let mut val 123; // 借用 val 用于函数调用。 borrows(val); // 借用已结束我们可以自由修改。 val 5; }这段代码蕴含三个层次的信息传入借用borrows(val)将val的可变绑定以共享引用i32的方式传入函数借用到期函数调用返回的那一刻传入的借用随即结束恢复可变性正因为借用已结束main中val 5的修改是合法的——若借用仍然存活此处会触发借用检查错误类似cannot assign to val because it is borrowed。从课程节奏看该小节仅分配 3 分钟授课时间其作用是为后续章节返回借用、多借用、生命周期标注铺垫借用流经函数边界的基本模型。它的核心结论可以凝练为一句话默认情况下函数参数借用不会逃逸出函数体这与后续returning-borrows中借用在函数返回时向外流动形成对照。二、借用可以向外流动返回引用的语义simple-borrows描述的是借用只进不出的最简情形但 Rust 函数完全可以返回引用使借用从函数流出。lifetimes/returning-borrows.md 专门讨论这一场景fn identity(x: i32) - i32 { x } fn main() { let mut x 123; let out identity(x); // x 5; // ️❌ x 仍然被借用 dbg!(out); }这里有两个关键点返回值延长借用identity把参数借用原样返回意味着x的借用从函数调用期间被延长到out最后一次使用为止。因此main中x 5的赋值会被编译器拒绝——借用检查器认为x仍然被out借用。返回引用必出自参数课程明确指出若一个函数返回引用或其它形式的借用它几乎必然派生自某个参数。这也是生命周期省略规则得以成立的前提——编译器需要能在参数与返回值之间建立对应关系。从源码结构可以推断returning-borrows中identity之所以能编译是因为它只有一个引用参数、一个引用返回值满足返回值绑定到唯一参数借用的省略规则详见第五节。而当参数不止一个时问题就变得复杂起来。三、多个借用参数编译器只信签名不看函数体当函数接收多个借用参数并返回其中一个时借用检查会遇到本质性困难。lifetimes/multiple-borrows.md 用一个刻意无法编译的示例引出问题fn multiple(a: i32, b: i32) - i32 { todo!(Return either a or b) } fn main() { let mut a 5; let mut b 10; let r multiple(a, b); // 哪一个仍被借用 // 两个修改是否都该被允许 a 7; b 7; dbg!(r); }这段代码因缺少生命周期标注无法编译但这正是教学意图所在。课程给出的推理线索包括函数体对借用分析不可见借用检查器分析函数调用点时并不会深入multiple的函数体去判断返回的是a还是b它只依据函数签名进行借用流分析。这是 Rust 借用系统签名即契约设计的核心体现。信息不足导致歧义签名fn multiple(a: i32, b: i32) - i32无法确定返回值究竟借用了a还是b调用点因此无法判断a 7与b 7是否合法。解决方式是显式生命周期标注fn multiplea(a: a i32, b: a i32) - a i32 { ... }引入泛型生命周期参数a后编译器被告知返回值的生命周期与a、b的借用生命周期绑定。这一场景的具体语义将在下一节的borrow-both与borrow-one中分化为两种策略。四、返回哪个借用borrow-both与borrow-one两种标注策略multiple-borrows提出的问题由课程中的两个姊妹小节给出两种典型解法两个参数共享一个生命周期borrow both或为返回的借用单独指定生命周期borrow one。4.1 Borrow Both返回引用同时借用两个参数lifetimes/borrow-both.md 展示要么返回a、要么返回b的场景fn picka(c: bool, a: a i32, b: a i32) - a i32 { if c { a } else { b } } fn main() { let mut a 5; let mut b 10; let r pick(true, a, b); // 哪一个仍被借用 // 两个修改是否都该被允许 // a 7; // b 7; dbg!(r); }核心语义有三点运行时无法确定pick依据布尔参数c决定返回a还是b编译期无法确定最终指向哪一个同一生命周期声明a、b与返回值共用同一个a等于告诉编译器返回值可能借用a和b两者双双冻结因此调用点r存活期间a与b都被借用即使运行时r只指向其中一个。取消注释a 7或b 7都会编译失败。课程还建议将第一个参数c换成其它条件验证结果不随返回分支而改变。4.2 Borrow One只借用一个参数精确锁定返回来源lifetimes/borrow-one.md 展示更精细的标注——多个借用参数但返回值只绑定其中一个#[derive(Debug)] struct Point(i32, i32); /// 在 points 中搜索距离 query 最近的点。 /// 假定 points 中至少有一个点。 fn find_nearesta(points: a [Point], query: Point) - a Point { fn cab_distance(p1: Point, p2: Point) - i32 { (p1.0 - p2.0).abs() (p1.1 - p2.1).abs() } let mut nearest None; for p in points { if let Some((_, nearest_dist)) nearest { let dist cab_distance(p, query); if dist nearest_dist { nearest Some((p, dist)); } } else { nearest Some((p, cab_distance(p, query))); }; } nearest.map(|(p, _)| p).unwrap() // query // 如果改成返回这个会怎样 } fn main() { let points [Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)]; let query Point(0, 2); let nearest find_nearest(points, query); // 此刻 query 未被借用。 drop(query); dbg!(nearest); }这个例子的要点两个生命周期并存points使用标注生命周期aquery则通过省略规则获得自己的隐式生命周期详见第五节。返回值a Point明确声明只借用pointsquery可提前释放因为返回值不借用querymain中在nearest仍存活时直接drop(query)完全合法函数体必须兑现契约课程特别设计了一个反例——若把函数体最后一行改成返回query编译器会报错因为query的生命周期与签名承诺的a不符。此时需要为query添加第二个生命周期参数b并观察新的错误信息。这印证了借用检查器会核对函数体逻辑确实返回了与签名生命周期一致的引用强制函数遵守签名所声明的契约。4.3 更深的探索生命周期子类型lifetime subtypingborrow-one小节的More to Explore还提出了两个进阶话题生命周期界限b: a错误信息中的 help 提示可以添加生命周期界限b: a表示b至少与a一样长这样就能合法返回query。这是生命周期子类型lifetime subtyping的一个实例——可以用一个更长的生命周期去满足一个较短生命周期的位置static引用返回static生命周期例如指向static变量的引用总是安全的因为static保证比任何其它生命周期都长任何期望较短生命周期的位置都可以用static引用顶上。这两点表明生命周期标注不仅是绑定关系的声明还暗含一套偏序关系长生命周期可被当作短生命周期使用这也是理解复杂泛型代码的基础。五、生命周期省略规则语法简写而非类型推断在函数签名层面Rust 要求生命周期必须被完整指定但绝大多数情况下允许省略elision。lifetimes/lifetime-elision.md 明确指出这是语法简写不是类型推断。省略规则只有三条每个没有生命周期标注的参数都被赋予一个独立的生命周期如果只有一个参数生命周期它被赋予所有未标注的返回值如果有多个参数生命周期但第一个是self则该生命周期被赋予所有未标注的返回值。课程用三个函数演示三条规则的应用fn only_args(a: i32, b: i32) { todo!(); } fn identity(a: i32) - i32 { a } struct Foo(i32); impl Foo { fn get(self, other: i32) - i32 { self.0 } }逐一对照规则函数省略后的完整形式适用规则only_argsfn only_argsa, b(a: a i32, b: b i32)规则 1无返回值仅补全参数identityfn identitya(a: a i32) - a i32规则 1 规则 2唯一参数生命周期流向返回值Foo::getfn geta, b(a self, other: b i32) - a i32规则 3self的生命周期流向返回值课程强调若应用三条规则后仍有生命周期未填满编译器会报错并要求手工添加标注——这正是multiple-borrows中fn multiple(a: i32, b: i32) - i32无法编译的原因两个参数生命周期、一个返回值规则 2 与规则 3 均不适用必须显式写出。六、数据结构中的生命周期存储借用数据的注解借用并不只存在于函数边界数据结构也可以持有借用数据此时类型本身必须携带生命周期注解。lifetimes/struct-lifetimes.md 的示例#[derive(Debug)] enum HighlightColor { Pink, Yellow, } #[derive(Debug)] struct Highlightdocument { slice: document str, color: HighlightColor, } fn main() { let doc String::from(The quick brown fox jumps over the lazy dog.); let noun Highlight { slice: doc[16..19], color: HighlightColor::Yellow }; let verb Highlight { slice: doc[20..25], color: HighlightColor::Pink }; // drop(doc); dbg!(noun); dbg!(verb); }要点总结注解语义struct Highlightdocument上的生命周期声明强制Highlight实例所引用的底层数据document str至少存活到该实例最后一次使用结构体不能活得比它引用的数据更久错误示范若在noun、verb仍存活时drop(doc)取消注释即可验证借用检查器会直接报错实用取舍携带借用数据的类型迫使使用者一直持有原始数据适合做轻量视图lightweight view但通常更难使用。课程建议能拥有数据就直接拥有own your data把借用放进结构体作为默认以外的选择多生命周期标注当结构体内多个引用之间也需要描述生命周期关系时可以有多个生命周期参数——这类用例属于非常进阶的场景。七、课程中的配套练习Protobuf 解析与借用贯穿lifetimes 章节以一道 30 分钟的综合练习收尾lifetimes/exercise.md 要求实现一个 Protobuf 二进制编码解析器。该练习与借用的联系在于零拷贝切片传递解析函数通过传递字节切片slice共享底层数据底层数据从不被复制——这正依赖于借用与切片引用机制典型借用流模式解析过程中函数签名大量出现[u8]与返回值引用是本章借用流入、流出函数模型的实战演练失败处理简化练习说明指出正常 Rust 代码应使用Result处理解析失败如i32解析时缓冲区不足 4 字节练习中为简化统一使用panic而错误处理主题留到课程 Day 4 的 error-handling 章节专门讲解参见 error-handling 目录。练习模板通过{{#include exercise.rs:preliminaries }}等标注直接嵌入源码片段完整可编辑版本位于 lifetimes/exercise.rs解法见 lifetimes/solution.md。八、小结以签名为契约的借用流模型回顾整个章节借用与生命周期的知识可以收敛为一条主线最简模型simple-borrows借用默认只持续到函数调用结束返回即释放向外流动returning-borrows返回引用会延长对应参数的借用歧义与标注multiple-borrows、borrow-both、borrow-one多个借用参数 返回引用时必须用生命周期标注消除歧义——共用a表示两个都被借用分立生命周期表示只借用一个省略规则lifetime-elision三条规则补齐绝大多数省略场景规则补不满时编译器强制显式标注数据结构struct-lifetimes持有借用数据的类型必须声明生命周期且尽量优先选择拥有数据实战印证exercise通过 Protobuf 解析练习把切片零拷贝传递 借用流出函数落实到真实代码。贯穿始终的核心理念是借用检查器只依据函数签名分析借用流函数体必须兑现签名声明的契约。理解这一点就能在读、写、修 Rust 代码时快速定位生命周期相关编译错误并从编译器为何报错反推出正确的标注方式。本课程的 lifetimes 模块共 9 个文件含练习与解法在 src/lifetimes/ 目录下可完整查看全课程目录结构见 src/SUMMARY.md。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考