所有权代码评审的借用边界
Rust 的借用检查能保证引用不会活得比数据更久,但它并不替评审者决定一个函数“应该借用还是应该拥有”。签名里的&T、&mut T、T、Cow或Arc都在表达调用关系。选错了,代码也许能编译,却会出现多余复制、过长锁定范围,或把调用方绑到不必要的生命周期上。
输入能借用时,不急着接管所有权
只读取字符串的函数通常接收&str,比接收String更宽松。调用方可以传字符串字面量、String的切片或其他连续文本,而不必交出所有权:
fn is_empty(s: &str) -> bool { s.is_empty() }同样,读取一组元素时优先考虑&[T],需要修改元素但不改变容器归属时使用&mut [T]。评审公共接口时要问:函数是否真的需要保存输入、转移到其他线程或返回给调用方?如果答案是否定的,接收拥有值往往只是把调用成本向外推。
反过来,函数确实要把数据存进结构体、队列或异步任务时,接收拥有值可能更清楚。此时不必为了保留一个看似灵活的借用接口,在内部立即to_owned()。调用方本来就有可移动的值,移动通常没有额外分配;强制借用再复制,反而隐藏了真实成本。
返回借用要说清来源
返回引用的函数需要让生命周期关系可理解。若返回值来自某个输入,签名应明确绑定到那个输入;若可能来自两个不同来源,复杂生命周期可能是在提醒设计本身含糊。评审时不要急着通过增加生命周期参数让编译器闭嘴,先确定返回值究竟属于谁,以及调用方需要保存多久。
缓存、解析器和树形结构特别容易出现“为了避免复制,到处返回内部引用”的设计。这样会让一次借用覆盖很长调用链,后续想更新缓存或调整结构时处处受限。对小型结果,返回拥有值可能更便宜,也让模块边界更稳定。是否复制应结合数据大小和调用频率判断,而不是把“零复制”当成固定目标。
可变借用范围越短越好
&mut T表示一段时间内的独占访问。代码评审时,应检查可变借用是否跨过无关计算、日志或外部调用。借用范围过长会让其他字段无法使用,也容易逼出不必要的clone、内部可变性或大锁。可以先读取所需值,完成局部修改后尽早结束借用,再执行后续操作。
在异步函数中尤其要警惕跨await持有借用或锁守卫。等待期间任务可能长时间挂起,其他请求就无法访问共享状态。更合适的方式通常是锁内读取或更新最小状态,释放守卫后再等待 I/O;完成外部调用后,重新加锁并验证状态是否仍适合提交。
clone与共享所有权都要有理由
编译错误附近随手加clone()很常见,但评审者应追踪复制发生在什么数据上、位于循环还是请求入口。小型标识复制一次问题不大,大缓冲区在高频路径复制会直接变成内存带宽和延迟成本。Arc也不是免费逃生门:它延长对象寿命,引入引用计数,并可能掩盖真正需要独占修改的状态。
看到unsafe绕开借用规则时,审查范围要扩大到它依赖的全部不变量:指针是否对齐、别名是否受控、内存何时释放、跨线程是否安全。注释必须说明为什么这些条件始终成立,并用测试覆盖边界。单纯写一句“这里是安全的”没有审查价值。
借用边界评审的落点很具体:只读输入尽量接受借用,需要长期保存时明确接管所有权,可变访问缩到最小,跨异步等待不占住共享资源。编译器保证这些选择不会破坏内存安全,评审则要确认它们符合接口语义和运行成本。