mcpField
O
ownership-not-clone

ownership-not-clone

github.com/rewrite-rs开发1042 次安装10 周前更新
代码仓库

关于 ownership-not-clone

面向 Rust 开发者的编码技能,指导如何确定值的所有权与借用生命周期,而不是用 clone 回避借用检查器错误。它根据实际需求选择引用、拆分字段借用或共享所有权。

主要功能

  1. 01根据编译器错误诊断借用冲突
  2. 02优先使用引用而非不必要的所有权
  3. 03拆分不同字段的可变借用
  4. 04用 Cow 表示借用或拥有的值
  5. 05评估共享所有权是否必要
  6. 06通过 Clippy 和测试验证改动

适用场景

  1. 01审查为修复编译错误而添加的 clone
  2. 02调整函数签名以避免调用方克隆
  3. 03在可变操作中分别借用不同字段
  4. 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.md
SHARED-STATE.md
agents/
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('<', "&lt;").replace('>', "&gt;"))
    } 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 配置;其发现项只是候选建议,不是要直接应用的修改。

评论

支持 Markdown