コンテンツにスキップ

Rustのエラー E0502: cannot borrow as mutable because it is also borrowed as immutable

E0502 は、コードのどこかが参照を通してまだ値を読んでいる最中に、その値を書き換えようとした、という意味のエラーです。ベクタを使っているとしょっちゅう出会いますが、たいていは1行動かすか、値を1つコピーすれば直ります。このページでは、まず直し方を見せてから、Rust がなぜそこにこだわるのかを説明します。このシリーズのほかのエラーは、Rustのコンパイルエラー解説を見てください。

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 here
4 | v.push(4); // ❌ first が生きている間に Vec を書き換えている
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("First: {first}"); // ……ここで first を使っているから
| ----- immutable borrow later used here

1行目は「v は不変で借用されているので、可変で借用することはできない」という意味です。ラベルは順に「ここで不変の借用が起きる」「ここで可変の借用が起きる」「不変の借用があとでここで使われる」と言っています。

このエラーが出るのは、次のプログラムです。

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // Vec の中を指す参照
v.push(4); // ❌ first が生きている間に Vec を書き換えている
println!("First: {first}"); // ……ここで first を使っているから
}

コンパイラは3つの行を指しています。エラーを理解するには、3つ全部が必要です。

  1. 3行目: &v[0] は、v の中を指す読み取り専用の参照を作ります。これが不変の借用(immutable borrow)です。
  2. 4行目: v.push(4) は v を書き換える必要があるので、可変の借用(mutable borrow)をします。
  3. 5行目: first がもう一度使われています。この行のせいで、最初の借用が4行目をまたいで生き続けています。

5行目がなければ、このコードはコンパイルできます。問題は2つの借用があることではなく、2つの借用が重なっていることです。

ルールは「読む人はたくさんいてもいいし、書く人は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: 1
Now: [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つ増える分、少しメモリを使いますが、コードはわかりやすく、正しいことが一目でわかります。たいていは割に合う取引です。

ほかの直し方が合わないときの最後の手段です。インデックスはただの数値で、借用ではないので、書き換えの邪魔をしません。

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)のほうが安全です。

ループで回している 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を見てください。

  • E0499:可変の借用が同時に2つある。読む人と書く人ではなく、書く人が2人いるという、同じ考え方のエラーです。
  • E0382:値をムーブしたあとで使った。E0502 を、借用ではなくムーブで直そうとしたときによく出ます。
  • E0106:参照を返す関数に、ライフタイムの指定がない。
  • コンパイルエラーの一覧:エラー解説ページの全リストです。

理解度チェック

1. let first = &v[0]; v.push(4); println!("{first}"); がコンパイルできないのはなぜですか?

2. Vec<i32> から 50 未満の要素をすべて取り除きたいとき、E0502 を避けるいちばんすっきりした方法はどれですか?

3. 今の Rust では、不変の借用はいつ終わりますか?

スコア: 0 / 3