综合 Rust 课程(comprehensive-rust)生命周期实战:函数返回引用(Returning Borrows)的借出与生命周期标注
发布时间:2026/9/11 21:44:10 作者:尧图编辑部 阅读量:1,286
生命周期实战:函数返回引用(Returning Borrows)的借出与生命周期标注)
综合 Rust 课程comprehensive-rust生命周期实战函数返回引用Returning 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本篇技术指南以 Google Android 团队维护的 Rust 课程 comprehensive-rust 中 lifetimes/returning-borrows.md 为核心讲解 Rust 函数如何把借用带出函数体——即返回引用时借用如何从参数流向返回值、为何返回值会延长参数的借用期并结合课程中生命周期省略elision、单一借用返回、多借用选择返回等进阶示例配合仓库内exercise.rs源码级佐证帮助读者彻底理解返回引用背后的借用检查机制与显式生命周期标注语法。一、从借入到借出借用跨越函数边界在 Rust 中借用borrow可以流入函数调用borrows(val)时val作为参数把对val的借用带进了函数体内。这是课程 lifetimes/simple-borrows.md 中演示的最简单场景——借用只持续到函数调用结束一旦函数返回借用即告终止调用方可以自由地再次修改该变量fn borrows(x: i32) { dbg!(x); } fn main() { let mut val 123; // Borrow val for the function call. borrows(val); // Borrow has ended and were free to mutate. val 5; }这里的借用是瞬时的借用检查器borrow checker只需确认在borrows调用期间val没有被修改即可。但函数不仅可以接收借用还可以返回引用。此时情况发生了本质变化借用不再随函数调用结束而消亡而是从函数内部流回调用方形成了一个跨越函数边界的借用关系。这正是 returning-borrows.md 的核心主题。二、核心示例identity函数返回引用原文档给出的最小示例是identity——一个原样返回输入的恒等函数fn identity(x: i32) - i32 { x } fn main() { let mut x 123; let out identity(x); // x 5; // ️❌ x is still borrowed! dbg!(out); }这个示例直观地展示了借用流回函数的三条关键事实原文档details中的教学要点Rust 函数可以返回引用这意味着借用能够从函数中流回调用方如果函数返回一个引用或其它形式的借用它几乎必然派生自某个参数。由于返回值与输入数据共享同一块内存返回值的存活会延长对应参数借用的时长本例仍然相当简单只有一个借用被传入函数因此返回的借用必然就是这一个。最值得注意的行为在第 28 行x 5;被注释掉旁边标注了❌ x is still borrowed!。因为out持有从identity(x)派生出的引用且out在dbg!(out)之前一直存活所以x的可变借用窗口被整个延长了——借用检查器会拒绝在out存活期间对x进行任何修改。这正是返回值延长参数借用的直接后果。三、为什么identity不需要写生命周期标注Lifetime Elision 规则细心的读者会发现identity(x: i32) - i32并没有显式的生命周期标注却能通过编译。这是因为 Rust 提供了**生命周期省略elision**机制。课程 lifetimes/lifetime-elision.md 指出函数参数与返回值的生命周期必须被完整确定但 Rust 允许在大多数情况下按三条简单规则省略标注。注意这不是类型推断inference而是一种语法简写每个没有生命周期标注的参数都会被赋予一个独立的生命周期如果只有一个参数生命周期它会被赋予所有未标注的返回值如果有多个参数生命周期但第一个是self即方法则该self的生命周期被赋予所有未标注的返回值。对identity而言规则 2 直接生效唯一的参数x: i32获得一个隐式生命周期a返回值i32也自动获得同一个a。于是函数签名等价于fn identitya(x: a i32) - a i32 { x }即返回的引用与输入的引用寿命一样长。若把三条规则全部套用后仍有生命周期未确定编译器会报错并要求手动补全标注——这就是下一节显式标注的用武之地。课程中 lifetimes/lifetime-elision.md 还给出了另外两个配合练习的例子fn only_args(a: i32, b: i32) { todo!(); } struct Foo(i32); impl Foo { fn get(self, other: i32) - i32 { self.0 } }only_args只借入不借出由规则 1 单独完成identity由规则 2 完成Foo::get是方法self是第一个参数由规则 3 完成。四、显式标注从多个借用中返回其中一个单一借用返回省略规则只能处理唯一候选的情况。当函数接收多个借用、却只返回其中一个时就必须用显式生命周期标注告诉编译器返回的借用到底对应哪个参数。课程 lifetimes/borrow-one.md 用find_nearest演示了这一场景——它接收points与query两个借用但只返回points中的元素#[derive(Debug)] struct Point(i32, i32); /// Searches points for the point closest to query. /// Assumes theres at least one point in 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 // What happens if we do this instead? } 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 isnt borrowed at this point. drop(query); dbg!(nearest); }关键点在签名fn find_nearesta(points: a [Point], query: Point) - a Pointa同时标注了points与返回值显式地把返回的借用绑定到points上query没有任何生命周期标注也不与返回值关联因此返回的引用不会借用query正因如此main里才能在nearest仍然存活时安全地drop(query)——编译通过即证明了返回值与query无关。课程教学要点还建议做两个破坏性实验来加深理解把函数最后一行改成return query;编译器会报错——因为query与返回值之间没有生命周期关联给query加上第二个生命周期b如query: b Point后再看错误信息。借用检查器会验证函数体内的实际逻辑确实返回了具有正确生命周期的引用从而强制函数实现遵守签名约定的契约。错误提示中的 help 信息还会提到一种解法添加生命周期约束b: a读作 b至少存活到a那么久表示b比a活得更长从而允许返回query。这属于生命周期子类型化lifetime subtyping在期望较短生命周期的地方返回一个更长的生命周期。同理返回static生命周期如对static变量的引用也总是安全的因为static保证比任何其它生命周期都长。五、两个都可能被返回多个借用共享一个生命周期如果函数运行时既可能返回a也可能返回b取决于运行时条件编译期无法预知具体返回哪个该怎么办课程 lifetimes/borrow-both.md 给出的答案是让两个参数共享同一个生命周期并与返回值绑定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); // Which one is still borrowed? // Should either mutation be allowed? // a 7; // b 7; dbg!(r); }签名fn picka(c: bool, a: a i32, b: a i32) - a i32的含义是a与b使用同一个a返回值也使用a。这意味着返回的引用同时借用了a和b两个变量——即使运行时它只指向其中一个。体现在调用方把第 32、33 两行注释取消后a 7与b 7都会报错因为r借用了两者。无论把c改为true还是false、运行时实际返回哪一个结果都一样——借用关系由签名决定而不是由运行路径决定。与之对照的是课程 lifetimes/multiple-borrows.md 中展示的待修复状态当multiple(a: i32, b: i32) - i32缺少标注时省略规则无法确定返回值来自a还是b两个参数生命周期候选且都不是self代码无法编译。这正是需要读者停下来思考的时机哪个参数借用应该被返回值延长是否需要看到函数体才能判断答案是借用检查器在分析函数时只看签名不看函数体——函数体是实现细节签名才是对外契约。修复方法是把两个参数与返回值统一标注为afn multiplea(a: a i32, b: a i32) - a i32 { ... }六、源码佐证真实项目中返回的借用课程练习源码返回引用绝不只是语法演示而是 comprehensive-rust 课程练习中大量使用的实战模式。以 lifetimes 章节的 Protobuf 解析练习 src/lifetimes/exercise.md 及其实例代码 src/lifetimes/exercise.rs 为例练习的目标是解析 protobuf 二进制编码其核心思想就是底层数据从不复制只传递切片引用。parse_field同时返回解析出的字段和剩余未消费的字节切片——两处返回都携带借用// 摘自 src/lifetimes/exercise.rs:parse_field fn parse_field(data: [u8]) - (Field_, [u8]) { let (tag, remainder) parse_varint(data); let (field_num, wire_type) unpack_tag(tag); let (fieldvalue, remainder) match wire_type { WireType::Varint { let (value, remainder) parse_varint(remainder); (FieldValue::Varint(value), remainder) } WireType::Len { let (len, remainder) parse_varint(remainder); let len len as usize; // cast for simplicity let (value, remainder) remainder.split_at(len); (FieldValue::Len(value), remainder) } }; (Field { field_num, value: fieldvalue }, remainder) }这里Field_中的_是生命周期占位符表示其生命周期与入参data: [u8]相关联。WireType::Len分支通过remainder.split_at(len)直接切出输入缓冲区的子切片没有任何数据拷贝——这正是返回的借用派生自参数的真实写照。借用进一步传播到整个数据结构FieldValuea、Fielda都携带生命周期参数见 exercise.rs 中enum FieldValuea与struct Fielda的定义parse_messagea, T: ProtoMessagea则把输入切片的生命周期a贯穿到整个解析结果中// 摘自 src/lifetimes/exercise.rs:parse_message fn parse_messagea, T: ProtoMessagea(mut data: a [u8]) - T { let mut result T::default(); while !data.is_empty() { let parsed parse_field(data); result.add_field(parsed.0); data parsed.1; } result }Persona与PhoneNumbera结构体把a str字段存进自己的定义中保证解析结果引用的字符串底层数据与输入缓冲区同生命周期。这与课程 lifetimes/struct-lifetimes.md 的原则完全一致如果数据结构存储借用数据就必须标注生命周期且结构体不能活得比它引用的数据更久。练习中提供的多个#[test]如test_id、test_full_person均对字面量字节数组调用parse_message验证了从输入切片返回并存储借用这条链路的正确性。七、小结返回引用时的判断清单综合本课程的多个示例返回引用时建议按以下思路排查借用能否省略标注先看是否满足三条省略规则唯一参数生命周期或方法self满足则无需显式标注返回哪个参数的借用若多个借用候选且只返回一个用显式a把返回值绑定到正确的参数上见 borrow-one.md是否可能返回多个参数之一若编译期无法确定则让所有候选参数共享同一生命周期返回值同时借出见 borrow-both.md记住契约在签名上借用检查器依据签名而非函数体判定借用关系见 multiple-borrows.md数据结构中的借用借用要存入结构体时为结构体标注生命周期并确保底层数据存活见 struct-lifetimes.md 与 exercise.rs。记住核心结论函数返回引用等于把参数的借用带出了函数返回值存活多久对应参数的借用就被延长多久。在 comprehensive-rust 课程的借出与生命周期章节中从identity的单参数简写到find_nearest的显式绑定再到pick的多借用共享这一原则贯穿始终。【免费下载链接】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),仅供参考