Rust 借用エラー総まとめ — value moved、cannot borrow、does not live long enough

読了 5分

Rust を学び始めた頃の体感難易度は、そのほとんどが借用チェッカー(borrow checker)との押し問答から来ます。しかし実際に出会うエラーの種類は指で数えられるほど少なく、それぞれの対処も定型化されています。この記事は、その代表的なエラーをコード(E 番号)別に整理した解決集です。原理を初めて学ぶ方は、所有権借用の回を先に読むことをお勧めします。

E0382: value borrowed here after move — 移動した値を使った #

src/main.rs
let name = String::from("レポート");
let title = name;
println!("{name}"); // error[E0382]: borrow of moved value: `name`

ヒープを所有する値の代入・関数への受け渡しは移動で、元の変数は無効になります。対処は状況別に 3 つです。

  • 読むだけでよかったなら: 移動の代わりに参照を渡します。let title = &name;、または関数の引数を &str に。
  • 本当に 2 部必要なら: let title = name.clone(); でコピーコストを明示します。
  • 関数が所有権を持っていったなら: 引数を参照に変えるのが先で、だめなら戻り値で返してもらいます。

ループで for x in vec とコレクションごと移動させた後にもう一度使うミスも同じエラーです。for x in &vec と借りて反復すれば解決します。

E0502: 不変借用と可変借用が重なった #

src/main.rs
let first = &items[0];            // 不変借用の開始
items.push(99);                   // error[E0502]: cannot borrow `items` as mutable
println!("{first}");              // 不変借用はここまで生きている

「読んでいる人がいる間は変更不可」という規則そのままです。鍵になる知識は、借用は最後に使われた地点までしか生きていないということです。だから最初の対処は順序の入れ替えです。

src/main.rs
let first = items[0];  // 参照の代わりに値をコピー (Copy 型のとき)
items.push(99);        // 問題なし

読み取りを先に終わらせる(最後の使用を変更の前に移す)か、参照の代わりに値をコピーして借用自体をなくす方向です。反復中の変更なら「変更対象を集めておいて反復後に反映」が定石です。

E0499: 可変借用が 2 つある #

src/main.rs
let a = &mut scores[0];
let b = &mut scores[1]; // error[E0499]: cannot borrow `scores` as mutable more than once

2 つの要素が別々のマスでも、コンパイラから見れば同じコレクションへの可変借用 2 つです。インデックスが重ならないことを証明できないからです。標準ライブラリが、この場合のための安全な刃物を用意しています。

src/main.rs
let (left, right) = scores.split_at_mut(1);
let a = &mut left[0];
let b = &mut right[0]; // 重ならない 2 つの断片

2 要素の入れ替えが目的なら、scores.swap(0, 1) のように目的に合ったメソッドがすでにあることも多いです。

E0506: 借用中の値に代入した #

src/main.rs
let entry = &user.name;
user.name = String::from("新しい名前"); // error[E0506]: cannot assign to `user.name`
println!("{entry}");

参照が生きている値を丸ごと入れ替えようとした場合です。E0502 と同じ原理で、対処も同じです。参照の最後の使用を代入の前に移すか、参照の代わりに必要な部分をコピーしておきます。

E0597: borrowed value does not live long enough — 値が参照より先に死ぬ #

src/main.rs
let best;
{
    let scores = vec![90, 85];
    best = &scores[0];
} // error[E0597]: `scores` does not live long enough
println!("{best}");

参照が指す値がスコープを抜けて解放されるのに、参照はもっと長く使われようとしている場合です。関数でローカル変数の参照を返す古典的な事例が同じ系統(E0106 と一緒)です。対処は方向転換です。参照を長生きさせようとせず、所有権を外へ移します。 上の例なら best = scores[0];(Copy)か let scores を外側のスコープへ引き上げること、関数なら &String ではなく String を返すことです。ライフタイム注釈('a)は値を長生きさせる道具ではなく関係の宣言にすぎない(基礎第 9 回)と覚えておくと、注釈を足す側ではなく所有権を移す側が答えである場合を見分けられます。

E0596: クロージャがキャプチャした変数を変更しようとした #

src/main.rs
let mut count = 0;
let inc = || count += 1; // クロージャの宣言自体は通る
some_parallel_api(inc);  // error[E0596]: captured variable in a `Fn` closure

逐次コードなら、クロージャを let mut inc = move || ... に変えるか、FnMut を受け取る API を使えば解決します。しかし並列 API(rayon など)でこのエラーが出たなら話が別です。複数のスレッドが同じ変数を同時に変更しようとする設計、つまりデータ競合をコンパイラが止めたのです。このときの対処はクロージャの修繕ではなく構造の変更です。実践第 5 回で扱ったとおり、スレッドごとの部分結果を作って統合する(fold・reduce)のが定石です。

共通のコツ #

  • help 行から読みます。 Rust のエラーの半分は処方箋を同封していて、rustc --explain E0382 が詳しい解説を見せてくれます。
  • エラーを消す最小の手段を選びます。 参照で足りる場所に clone を使えば動きはしますがコストが残り、clone で足りる場所にライフタイム注釈を持ち込めば複雑さが残ります。参照 → 順序の入れ替え → コピー(clone)→ 構造の変更、と軽いものから試すのがお勧めです。
  • 借用チェッカーは最後ではなく最初に会うレビュアーです。 ここに挙げたエラーの大半は、他の言語ならランタイムのバグ(無効な参照、反復中の変更、データ競合)として遅れて現れたはずのコードです。

まとめ #

  • 移動後の使用(E0382)は、参照・clone・返却のうち状況に合う最小の手段で解決します。反復は &vec が基本です。
  • 借用の衝突(E0502・E0506)は「借用は最後の使用まで」を利用して、読み取りを前へ、変更を後ろへ並べ替えるのが最初の対処です。
  • 同じコレクションへの可変借用 2 つ(E0499)は、split_at_mutswap のような標準ライブラリの安全な分割道具が答えです。
  • ライフタイム不足(E0597)は、参照を生かす方向ではなく所有権を移す方向で解決します。
  • 並列クロージャの E0596 はバグ報告ではなくデータ競合の遮断です。fold・reduce の構造に変えます。
X