关于 ownership-not-clone
面向 Rust 开发者的编码技能,指导如何确定值的所有权与借用生命周期,而不是用 clone 回避借用检查器错误。它根据实际需求选择引用、拆分字段借用或共享所有权。
主要功能
- 01根据编译器错误诊断借用冲突
- 02优先使用引用而非不必要的所有权
- 03拆分不同字段的可变借用
- 04用 Cow 表示借用或拥有的值
- 05评估共享所有权是否必要
- 06通过 Clippy 和测试验证改动
适用场景
- 01审查为修复编译错误而添加的 clone
- 02调整函数签名以避免调用方克隆
- 03在可变操作中分别借用不同字段
- 04判断共享状态是否需要 Rc 或 Arc
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill ownership-not-clone安装后在 Claude Code 中输入/ownership-not-clone确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CLONE-DECISIONS.mdSHARED-STATE.mdagents/openai.yaml详细介绍
所有权,而非克隆
借用检查器指出了所有权处理不当之处,而不是一个需要绕开的障碍。这项技能决定由谁持有值,以及持有多久。
规则
每个 clone 都必须能用一句话解释,而且不能是“借用检查器报错了”。如果克隆解决了实际需求——某个值必须比借用存活得更久——那就没问题;如果克隆只是为了消除报错,那就是把设计决策推迟了。
读懂错误,而不是绕过它
E0502(不可变借用仍然有效时进行可变借用)和 E0499(对同一位置进行两次可变借用)指出了生命周期冲突;修复应针对错误描述的结构,而不是消除报错的 clone:
// No clone: `first` is Copy, so the borrow ends at the let.
let first = items[0];
for _ in 0..n {
items.push(first);
}
拆分借用
借用检查器会在方法调用中跟踪整个值,但直接访问字段时会分别跟踪各个字段。&mut self 方法还需要访问 self.other_field,这是典型的虚假冲突——只需解构一次:
// The two fields are separate lets and borrow independently.
impl Server {
fn handle(&mut self) {
let Self { connections, log, .. } = self;
for conn in connections.iter_mut() {
log.record(conn.id());
}
}
}
split_at_mut 会返回同一个切片中两个互不重叠的 &mut 部分;如果解构还不够,就提取一个接收这两个字段的自由函数。
在参数位置选择够用且代价最低的类型
对于函数仅读取的路径,使用 &str 而非 &String、&[T] 而非 &Vec<T>、impl AsRef<Path> 而非 PathBuf。如果函数只读取数据,签名却要求 String,就会迫使每个调用方克隆——问题出在签名上。
// Forces a clone at every call site that holds a &str.
fn log_message(message: String) { ... }
// Reads only — callers pass a reference and copy nothing.
fn log_message(message: &str) { ... }
适用于真正需要借用或拥有的情况的 Cow
当常见路径原样返回输入,而少见路径需要分配内存时,Cow<'_, str> 才能发挥作用。
use std::borrow::Cow;
// The common path borrows; only input containing a bracket allocates.
fn sanitize(input: &str) -> Cow<'_, str> {
if input.contains(['<', '>']) {
Cow::Owned(input.replace('<', "<").replace('>', ">"))
} else {
Cow::Borrowed(input)
}
}
Rc/Arc/RefCell/Mutex 确实表达实际需求的情况
只有当运行时结构确实有多个所有者,且没有任何一个所有者的存活时间长于其余所有者时,共享所有权才合理;仅仅是单一所有者不方便命名,并不足以构成理由。添加 RefCell 前,先运行 SHARED-STATE.md 中的四问测试;决策表见 CLONE-DECISIONS.md;通过一句话测试的句柄克隆见 SHARED-STATE.md。
mem::take 和 mem::replace
mem::take 和 mem::replace 会将值从 &mut 中移出,并在原处留下默认值——这正是“我需要拥有这个值,但手头只有可变借用”时的答案:
let old = mem::take(&mut self.buffer);
避免静态变量
持有可变状态或延迟初始化状态的 static,其生命周期无人明确记录,所有权关系也无人能理清。传递该值,或将其保存在拥有这项工作的类型中。
延后处理
调整正确代码的表达方式属于 /idiomatic-rust;将不变量建模到类型中属于 /type-driven-design;错误类型属于 /rust-errors;跨越 .await 点的共享状态属于 /async-rust。
验证
保留或移除克隆不会改变行为,因此“这样仍然有效”的说法,应通过套件并按照仓库配置的 lint 级别来验证:
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo test
clippy::redundant_clone 属于 nursery,默认关闭——可通过单独运行 cargo clippy -- -W clippy::redundant_clone 来生成建议,但绝不要将它写入仓库的 lint 配置;其发现项只是候选建议,不是要直接应用的修改。