Rustのエラー E0502: cannot borrow as mutable because it is also borrowed as immutable
E0502 は、コードのどこかが参照を通してまだ値を読んでいる最中に、その値を書き換えようとした、という意味のエラーです。ベクタを使っているとしょっちゅう出会いますが、たいていは1行動かすか、値を1つコピーすれば直ります。このページでは、まず直し方を見せてから、Rust がなぜそこにこだわるのかを説明します。このシリーズのほかのエラーは、Rustのコンパイルエラー解説を見てください。
エラーメッセージ
Section titled “エラーメッセージ”error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &v[0]; // Vec の中を指す参照 | - immutable borrow occurs here4 | v.push(4); // ❌ first が生きている間に Vec を書き換えている | ^^^^^^^^^ mutable borrow occurs here5 | println!("First: {first}"); // ……ここで first を使っているから | ----- immutable borrow later used here1行目は「v は不変で借用されているので、可変で借用することはできない」という意味です。ラベルは順に「ここで不変の借用が起きる」「ここで可変の借用が起きる」「不変の借用があとでここで使われる」と言っています。
このエラーが出るのは、次のプログラムです。
fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; // Vec の中を指す参照 v.push(4); // ❌ first が生きている間に Vec を書き換えている println!("First: {first}"); // ……ここで first を使っているから}エラーの意味
Section titled “エラーの意味”コンパイラは3つの行を指しています。エラーを理解するには、3つ全部が必要です。
- 3行目:
&v[0]は、vの中を指す読み取り専用の参照を作ります。これが不変の借用(immutable borrow)です。 - 4行目:
v.push(4)はvを書き換える必要があるので、可変の借用(mutable borrow)をします。 - 5行目:
firstがもう一度使われています。この行のせいで、最初の借用が4行目をまたいで生き続けています。
5行目がなければ、このコードはコンパイルできます。問題は2つの借用があることではなく、2つの借用が重なっていることです。
Rust がこうする理由
Section titled “Rust がこうする理由”ルールは「読む人はたくさんいてもいいし、書く人は1人だけでもいい。でも両方を同時にはできない」です。借用のページで学んだルールですね。
Vec の場合は、とても具体的な理由があります。Vec は要素をひとつながりのメモリに並べて持っています。push したときにそのメモリがいっぱいだと、Vec は全部の要素をもっと大きな新しい場所に移して、古い場所を解放します。すると、古い場所を指していた参照(first など)は、解放済みのメモリを指すことになります。C++ では「イテレータの無効化」として有名なバグで、プログラムがクラッシュしたり、気づかないうちにでたらめな値を読んだりします。
友だちの住所をメモしておいたら、その友だちがもっと広い家に引っ越してしまった、というようなものです。メモは正しそうに見えますが、指しているのは空き家です。Rust は、あなたが空き家を訪ねてしまう前に、コンパイルを止めてくれるのです。
直し方1:書き換える前に読み終える
Section titled “直し方1:書き換える前に読み終える”まずはこれを選びましょう。Rust 2018 以降、借用が終わるのは閉じ括弧ではなく、最後に使われた場所です。これをNLL(non-lexical lifetimes)と呼びます。なので、ベクタを書き換える前に参照を使い終えれば大丈夫です。
fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; println!("First: {first}"); // first を使うのはここが最後なので、借用はここで終わる v.push(4); // ✅ もう誰も読んでいない println!("Now: {:?}", v);}実行結果:
First: 1Now: [1, 2, 3, 4](エラーになる例と比べやすいように、ここでは表示する文字列を英語のままにしています。)
直し方2:値をコピーして取り出す
Section titled “直し方2:値をコピーして取り出す”書き換えたあとに、古い値が必要なときはこれを選びます。参照ではなく、コピーを取っておきます。数値なら & を外すだけです。String なら .clone() を使います。
fn main() { let mut queue = vec![String::from("デプロイ"), String::from("テスト")]; let next = queue[0].clone(); // 参照ではなく、自分専用のコピー queue.push(String::from("リリース")); println!("次の作業: {next}"); println!("キュー: {:?}", queue);}実行結果:
次の作業: デプロイキュー: ["デプロイ", "テスト", "リリース"]Vec<i32> に対して let first = v[0]; と書けば、i32 は Copy なので、何もしなくてもコピーになります。
直し方3:iter_mut や retain で、その場で書き換える
Section titled “直し方3:iter_mut や retain で、その場で書き換える”for p in prices.iter() のようなループを書いて、その中で prices.remove(i) を呼んだり要素を書き換えたりしようとしたときは、これを選びます。それも E0502 になります。標準ライブラリには、まさにこのためのメソッドが用意されています。
fn main() { let mut prices = vec![120, 45, 300, 80];
// すべての要素をその場で書き換える for p in prices.iter_mut() { *p = *p * 9 / 10; // 10% 引き }
// いらない要素を取り除く prices.retain(|&p| p >= 50);
println!("{:?}", prices);}実行結果:
[108, 270, 72]iter_mut は要素1つにつき &mut を1つずつ渡してくれるので、書く人はいつも1人だけです。retain は、取り除く作業を1回の走査で安全にやってくれます。詳しくはベクタとイテレータを見てください。
直し方4:変更をいったん集めてから反映する
Section titled “直し方4:変更をいったん集めてから反映する”読んだ内容をもとに要素を追加したいときは、これを選びます。まず読んで集め、そのあと別のステップで書き込みます。
fn main() { let mut scores = vec![10, 25, 7, 40]; let bonuses: Vec<i32> = scores.iter().filter(|&&s| s > 20).map(|s| s * 2).collect(); scores.extend(bonuses); // 読み取りの借用は終わっているので、書き込める println!("{:?}", scores);}実行結果:
[10, 25, 7, 40, 50, 80]Vec が1つ増える分、少しメモリを使いますが、コードはわかりやすく、正しいことが一目でわかります。たいていは割に合う取引です。
直し方5:インデックスを使う
Section titled “直し方5:インデックスを使う”ほかの直し方が合わないときの最後の手段です。インデックスはただの数値で、借用ではないので、書き換えの邪魔をしません。
fn main() { let mut scores = vec![10, 25, 7]; for i in 0..scores.len() { if scores[i] > 20 { scores.push(scores[i] * 2); } } println!("{:?}", scores);}実行結果:
[10, 25, 7, 50]0..scores.len() はループが始まる前に1回だけ計算されるので、新しく追加した要素は回りません。インデックスで要素を削除するときは注意してください。位置がずれるので、要素を飛ばしてしまったり、パニックしたりすることがあります。削除するなら、retain(直し方3)のほうが安全です。
よく出会う場面
Section titled “よく出会う場面”ループで回している Vec に push するとき。 for s in &scores { scores.push(..) } と書くと、ループの間ずっと scores が借用されています。直し方:先に集めてからあとで extend する(直し方4)か、インデックスでループします(直し方5)。
フィールドをループで回しながら &mut self のメソッドを呼ぶとき。 impl ブロックの中でとてもよくあるパターンです。
fn audit(&mut self) { for item in &self.items { self.record(item); // ❌ cannot borrow `*self` as mutable because it is also borrowed as immutable }}self.record は、self.items も含めて self 全体を可変で借用します。直し方:別のフィールドを直接触ります(self.log.push(item.clone()))。Rust では、別々のフィールドなら同時に借用できるからです。または、先に要素を clone しておきます。詳しくは構造体を見てください。
HashMap の値を持ったまま insert するとき。 let apples = stock.get("apples").unwrap(); のあとで stock.insert(..) を呼び、そのあと apples を使うと E0502 になります。直し方:let apples = *stock.get("apples").unwrap(); や .copied() で値をコピーして取り出します。詳しくはHashMapを見てください。
関連するエラー
Section titled “関連するエラー”- E0499:可変の借用が同時に2つある。読む人と書く人ではなく、書く人が2人いるという、同じ考え方のエラーです。
- E0382:値をムーブしたあとで使った。E0502 を、借用ではなくムーブで直そうとしたときによく出ます。
- E0106:参照を返す関数に、ライフタイムの指定がない。
- コンパイルエラーの一覧:エラー解説ページの全リストです。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. let first = &v[0]; v.push(4); println!("{first}"); がコンパイルできないのはなぜですか?
2. Vec<i32> から 50 未満の要素をすべて取り除きたいとき、E0502 を避けるいちばんすっきりした方法はどれですか?
3. 今の Rust では、不変の借用はいつ終わりますか?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- E0502 — Rust error codes index(英語):このエラーの公式の説明です。
- ベクタで値のリストを保持する — The Rust Programming Language 日本語版:まさにこの
let first = &v[0]; v.push(..)の例と、それがエラーになる理由が載っています。 - 参照と借用 — The Rust Programming Language 日本語版:「読む人はたくさん、または書く人は1人」というルールの全体像です。
- Non-lexical lifetimes — The Edition Guide(英語):借用が閉じ括弧ではなく、最後に使われた場所で終わるようになった理由です。
Vec— 標準ライブラリのドキュメント(英語):retain、iter_mut、extendなど、ベクタを安全に書き換えるためのメソッドが載っています。