ownership-not-clone について
借用チェッカーのエラーを clone で回避せず、値の所有者と借用期間を決める Rust 向けのコーディングスキルです。実際の要件に応じて、参照やフィールドごとの借用、共有所有権を選びます。
主な機能
- 01コンパイラーエラーから借用競合を診断
- 02不要な所有権より参照を選択
- 03フィールドごとに可変借用を分割
- 04借用と所有を切り替える Cow の使用
- 05共有所有権の必要性を評価
- 06Clippy とテストで変更を検証
活用シーン
- 01コンパイルエラー対策の clone を見直す
- 02呼び出し側の clone を避ける関数シグネチャ
- 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(同じ場所への2つの可変借用)は、ライフタイムの競合を示しています。解決策はエラーを黙らせる clone ではなく、エラーが示す構造にあります。
// No clone: `first` is Copy, so the borrow ends at the let.
let first = items[0];
for _ in 0..n {
items.push(first);
}
借用の分割
借用チェッカーは、メソッド呼び出しを通じて値全体を追跡しますが、フィールドへの直接アクセスではフィールドごとに追跡します。self.other_field も必要とする &mut self メソッドは、典型的な誤検出による競合です。一度分解しましょう。
// 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 は、1つのスライスから互いに重ならない2つの &mut を返します。分解だけでは不十分な場合は、2つのフィールドを受け取るフリー関数を切り出してください。
引数には、要件を満たす最も軽い型を使う
関数が読み取るだけのパスには、&String ではなく &str、&Vec<T> ではなく &[T]、PathBuf ではなく impl AsRef<Path> を使います。読み取るだけなのに 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 が本当の要件を表す場合
共有所有権が妥当なのは、実行時の構造に本当に複数の所有者がいて、どれか1つの所有者がほかの所有者より長く存続することもない場合です。単一の所有者を名前で表すのが面倒なだけの場合ではありません。RefCell を追加する前に、SHARED-STATE.md にある4つの質問によるテストを実施してください。判断表は CLONE-DECISIONS.md にあり、一文で説明できるテストに合格するハンドルのクローンは SHARED-STATE.md にあります。
mem::take と mem::replace
mem::take と mem::replace は、元の場所にデフォルト値を残すことで、&mut から値を移動させます。「この値を所有する必要があるが、手元にあるのは可変借用だけ」という場合の解決策です。
let old = mem::take(&mut self.buffer);
static を避ける
可変状態や遅延初期化状態を保持する 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 設定には追加しないでください。検出結果は編集候補であり、修正そのものではありません。