CATHODE COASTaccess to tools
6 min read

borrow checker に怒られたら読む — Rust 所有権・借用・ライフタイムの直し方

所有権で詰まる典型エラー3種を「なぜ怒られたか→どう直すか」で整理。clone・参照・スマートポインタの選び方と、コンパイラを味方にする手順まで。

shareB!

Rust を書き始めて最初にぶつかる壁は、たいてい borrow checker です。エラーメッセージ自体は驚くほど親切なのですが、「とりあえず clone() を足したら通った」で先に進んでしまい、なぜ通ったのかが分からないまま次のエラーに当たる——という詰まり方をしがちです。

この記事では、所有権まわりで頻出する 3 種類のコンパイルエラーを取り上げ、それぞれ「なぜ怒られるのか」と「実務でどう直すのが妥当か」をセットで整理します。所有権のルールを暗記するのではなく、コンパイラのメッセージから直し方を逆引きできるようになることを目標にします。

対象は Rust の安定版(stable)です。所有権・借用・ライフタイムの規則は Rust 1.0 から変わっていないため、エディション(2015 / 2018 / 2021 / 2024)に依存しない内容です。

先にまとめ

  • 所有権のエラーは大きく 「ムーブ後の使用(E0382)」「共有と可変の衝突(E0502)」「ライフタイム(E0106 / E0597)」 の 3 系統に分類できます。
  • 直し方の優先順位は ①参照で借りる → ②スコープや構造を見直す → ③どうしても必要なら clone / スマートポインタclone を反射的に足すのは最後の手段です。
  • ライフタイム注釈は「新しい制約を追加する」のではなく「すでに存在する制約をコンパイラに教える」だけ、と捉えると読み解きやすくなります。
  • 迷ったら rustc --explain E0382 のようにエラーコードで公式の解説を引けることを覚えておくと、その場で自己解決できます。

所有権の 3 ルール(30 秒で)

まず土台だけ確認します。詳細は The Rust Programming Language の所有権の章が一次情報です。

  1. 各値には所有者(owner)となる変数がただ 1 つ存在する。
  2. 所有者は同時に 1 つだけ。代入や関数への引き渡しで所有権は移動(move)する。
  3. 所有者がスコープを抜けると、値は drop されて解放される。

この 3 つが「GC なしでメモリ安全」を成立させる仕組みで、違反はすべてコンパイル時に弾かれます。以下のエラーは、いずれもこのルールのどれかに触れています。

エラー 1: use of moved value(E0382)

もっとも多いのがこれです。StringVec のようなヒープを持つ値は、代入や関数呼び出しでムーブされ、元の変数は使えなくなります。

move.rs
fn main() {
    let s1 = String::from("cathode");
    let s2 = s1; // 所有権が s1 -> s2 へムーブ
 
    println!("{s1}"); // error[E0382]: borrow of moved value: `s1`
}

s1 はムーブ済みなので、コンパイラは「解放責任が二重になる」ことを防ぐために使用を禁止します。直し方は 3 段階で考えます。

まず参照で借りられないかを検討します。 値の中身を読みたいだけなら、所有権を渡さず & で借りれば元の変数はそのまま使えます。

borrow-instead.rs
fn print_len(s: &String) -> usize {
    s.len() // 借りているだけ。所有権は呼び出し側に残る
}
 
fn main() {
    let s = String::from("cathode");
    let n = print_len(&s);
    println!("{s}{n} バイト"); // s はまだ使える
}

参照では足りない(別々に所有権が必要)ときだけ clone します。 clone はヒープの複製を伴うため、ホットパスで多用するとコストになります。「通ったから良し」ではなく、複製が本当に必要かを毎回問うのがポイントです。

Copy トレイトを実装した型(i32boolchar など固定サイズのプリミティブ)は、そもそもムーブではなくコピーされるため、この問題は起きません。「ムーブされるのはヒープを持つ型」と覚えておくと予測が立ちます。

エラー 2: cannot borrow as mutable(E0502)

借用には「共有参照は何個でも、可変参照はちょうど 1 つ、両者は排他」という規則があります。これがデータ競合をコンパイル時に排除する核心です。

aliasing.rs
fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];    // 不変借用
    v.push(4);            // error[E0502]: 可変借用が必要だが不変借用が生きている
    println!("{first}");
}

pushv を可変で借りますが、その時点で first という不変借用がまだ生きているため衝突します。Vec は再確保で要素の番地が変わりうるので、これを許すと first がダングリング参照になりかねません。だからコンパイラが止めます。

直し方は「借用の生存期間を重ならせない」ことです。現在の Rust は NLL(Non-Lexical Lifetimes)により、借用は「最後に使われた場所」までしか生きません。したがって使用順序を入れ替えるだけで解決することがよくあります。

reorder.rs
fn main() {
    let mut v = vec![1, 2, 3];
    let first = v[0];   // usize は Copy。借用ではなく値のコピー
    v.push(4);          // もう不変借用は生きていない
    println!("{first}");
}

値をコピーで受けてしまえば借用自体が消えます。コピーが重い型なら、first を使い終える位置を push より前に移す、あるいはブロック { } で借用スコープを閉じる、といった調整で衝突を避けられます。借用と参照の規則は References and Borrowingにまとまっています。

エラー 3: ライフタイム(E0106 / E0597)

参照を関数から返したり、構造体に持たせたりすると、ライフタイム注釈を求められます。

longest.rs
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
    // error[E0106]: missing lifetime specifier
}

コンパイラは「返り値の参照が ab のどちらに由来するのか、どちらが先に消えても安全か」を判断できません。ここで書くライフタイム注釈は、新しい寿命を作るのではなく、「返り値は引数と同じだけ生きる」という既存の事実を明示するものです。

longest-fixed.rs
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

もう 1 つ多いのが does not live long enough(E0597)です。ローカル変数への参照を、その変数より長く使おうとすると出ます。

dangling.rs
fn make() -> &String {          // E0106 / 実質 E0597 の原因
    let s = String::from("x");
    &s                          // s は関数を抜けると drop される
}

この場合、参照を返すのが間違いです。所有権ごと返す(String を返す)のが正解で、ライフタイム注釈をいくら足しても解決しません。「注釈で直る問題か、設計で直す問題か」を見分けることが、ライフタイムと付き合う勘所です。詳細は Validating References with Lifetimesを参照してください。

実務での判断:参照・clone・スマートポインタ

同じ「値を共有したい」でも、状況によって最適解は変わります。反射的に clone する前に、次の表で選択肢を持っておくと判断が速くなります。

やりたいこと第一候補コスト・注意
中身を読むだけ&T(共有参照)ゼロコスト。まずこれを試す
一時的に書き換える&mut T(可変参照)同時に 1 つだけ。衝突したらスコープを見直す
別々に所有権が要るT::cloneヒープ複製のコスト。ホットパスでは避ける
複数箇所で共有所有Rc<T> / Arc<T>参照カウント。Arc はスレッド安全な分やや重い
共有しつつ書き換えRc<RefCell<T>>借用チェックが実行時に移る。パニックの可能性

Rc<RefCell<T>> は「所有権のルールを実行時に先送りする」ための道具で、コンパイル時の安全性を一部手放します。便利ですが、まず &&mut と設計変更で解けないかを先に考えるのが健全です。

ハマりどころ

  • clone で黙らせる癖がつく。 通るけれど、なぜ通ったのかを理解しないと同じ壁に何度も当たります。まず参照で解けないかを確認する習慣を。
  • エラーコードを読み飛ばす。 error[E0502] のようなコードは rustc --explain E0502 で公式解説が読めます。メッセージ末尾の候補(help:)もかなり的確です。
  • ライフタイム注釈を「増やせば通る」と誤解する。 注釈は事実の宣言であって制約の追加ではありません。参照を返すこと自体が誤りなケースでは、いくら注釈しても通りません。

まとめ

Rust の所有権は、覚えるべきルールは 3 つだけで、エラーも 3 系統に整理できます。ムーブ後の使用は参照で、共有と可変の衝突は借用スコープの見直しで、ライフタイムは「注釈で足りるか設計を変えるべきか」の切り分けで対処する——この地図があれば、コンパイラのメッセージは敵ではなく、最短ルートを示す道案内になります。

  • 向いている人:GC のない言語で安全性をコンパイル時に担保したい人、既存の C/C++ 資産を安全側に寄せたい人。
  • 向いていないケース:プロトタイプを最速で回したいだけの用途では、借用チェックの学習コストが先行して重く感じられることがあります。

次の一歩としては、公式の The Rust Programming Languageを所有権の章(第 4 章)とライフタイムの章(第 10 章)だけでも通読しておくと、この記事のエラー対処が体系として腑に落ちます。