📚 背景知識(読んでから問題へ)
Day 003〜006では「所有権のムーブ」を学びました。値を関数に渡すと所有権ごと移動し、呼び出し元ではもう使えなくなる——これがRustの基本ルールでした。しかし、これでは不便な場面が多くあります。
fn calculate_length(s: String) -> usize {
s.len()
}
fn main() {
let text = String::from("hello");
let len = calculate_length(text);
// ここで text はもう使えない(ムーブされたので)
println!("{}", text); // コンパイルエラー!
}
calculate_lengthは文字列の長さを知りたいだけなのに、Stringの所有権ごと奪ってしまいます。呼び出し元でtextを使い続けたいなら、Day 005で見たTake & Returnパターンのように(String, usize)のタプルで返す必要がありますが、これは冗長です。
借用(Borrowing)——所有権を渡さずに値を使わせてもらう
Rustはこの問題を借用という仕組みで解決します。&を付けて参照を渡せば、所有権を移動させずに値へのアクセスだけを貸し出せます。
fn calculate_length(s: &String) -> usize {
s.len()
}
fn main() {
let text = String::from("hello");
let len = calculate_length(&text); // &text で「借用」を渡す
println!("{} is {} bytes", text, len); // text はまだ使える!
}
&textは「textの所有権を渡すのではなく、一時的に参照を貸す」という意味です。calculate_lengthが終わってs(参照)がスコープを抜けても、貸した本体(textが持つString)は破棄されません。あくまで借りているだけだからです。
不変借用(&T)と可変借用(&mut T)
参照には2種類あります。
| 種類 | 構文 | 何ができるか |
|---|---|---|
| 不変借用(共有参照) | &T | 読み取りのみ。同時に何個でも存在できる |
| 可変借用(排他参照) | &mut T | 読み取り・書き換え可能。同時に1個しか存在できない |
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut greeting = String::from("hello"); // mut が必要
append_world(&mut greeting); // &mut greeting で可変借用を渡す
println!("{}", greeting); // "hello, world"
}
&mutで借りるには、借用元の変数自体がlet mutで宣言されていなければなりません。また、Rustには借用規則という重要な制約があります。
&T」または「1個だけの&mut T」のどちらか一方しか同時に存在できないこの規則により、「誰かがデータを読んでいる最中に、別の誰かがそのデータを書き換えてしまう」というデータ競合がコンパイル時に排除されます。C++やGoでは実行時(あるいは-race検出ツールなど)でしか気づけない類のバグを、Rustはコンパイラが型チェックの一部として静的に防ぎます。
📝 問題
- 概念理解課題: 上記の
calculate_length(s: String)版とcalculate_length(s: &String)版を比較し、借用を使うことでDay 005のTake & Returnパターン(fn f(s: String) -> (String, usize)のように所有権を返し直す設計)が不要になる理由を説明してください - 実装課題(不変借用):
fn calculate_length(s: &String) -> usizeを実装し、main内でStringを作成→calculate_length(&text)を呼び出す→呼び出し後もtextをそのままprintln!で表示できることを確認するコードを書いてください - 実装課題(可変借用):
fn append_world(s: &mut String)を実装し、s.push_str(", world")で文字列を書き換えてください。main内でlet mut greetingを用意し、append_world(&mut greeting)を呼び出した後のgreetingを表示してください - デバッグ予測課題: 以下のコードはコンパイルエラーになります。発生するエラーコード(
E0xxx)と、なぜ許されないのかを「借用規則」の観点で予想してくださいfn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // ここでエラー println!("{}, {}", r1, r2); } - 発展の概念理解(コードは不要): もし仮にRustが「同時に複数の
&mut」を許してしまったら、マルチスレッドで同じデータに対して2つの&mut Stringを別スレッドが持ってしまうケースを考えてください。どのようなバグが起こり得ますか?(ヒント: 片方がpush_strで文字列を伸ばしてメモリを再確保している最中に、もう片方が同じメモリを読み書きしたら?)
🔍 ヒント(段階的開示)
ヒント1 — 方向性
&Tは「読むだけの窓口」、&mut Tは「書き換えてよい唯一の窓口」というイメージを持つと分かりやすいです- 借用規則は「本の貸し借り」に例えられます。同時に何人も本を読むことはできますが、誰かが書き込み中(可変借用)のときは、他の誰も読むことも書くこともできません
- 問題5は「2つの
&mutが同時に同じメモリを操作したら何が起こるか」を、VecやStringが内部でメモリの再確保(realloc)を行うことを踏まえて考えてください
ヒント2 — アプローチ
- 問題2・3は「背景知識」セクションのコード例をほぼそのまま
mainに書けば動きます - 問題4のエラーは「1個だけの
&mut Tしか同時に存在できない」という借用規則そのものへの違反です。エラーコードはDay 003〜006で見たE0382とは別系統(ムーブではなく借用のエラー)です &mutで借用するには、借用元の変数にmutが付いている必要があります(let mut s = ...)。mutを付け忘れると別のエラー(E0596)になるので注意してください
ヒント3 — コード骨格
fn calculate_length(s: &String) -> usize {
s.len()
}
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
// 問題2: 不変借用
let text = String::from("hello");
let len = calculate_length(&text);
println!("{} is {} bytes", text, len);
// 問題3: 可変借用
let mut greeting = String::from("hello");
append_world(&mut greeting);
println!("{}", greeting);
// 問題4は実際にはコメントアウトのまま、エラーを予想するだけでよい
// let mut s = String::from("hello");
// let r1 = &mut s;
// let r2 = &mut s; // ここでエラーになるはず
// println!("{}, {}", r1, r2);
}
✅ 模範解答
fn calculate_length(s: &String) -> usize {
s.len()
}
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
// --- 実装課題2: 不変借用 ---
let text = String::from("hello");
let len = calculate_length(&text); // &text で借用を渡す
println!("{} is {} bytes", text, len); // text はムーブされていないのでまだ使える
println!("---");
// --- 実装課題3: 可変借用 ---
let mut greeting = String::from("hello"); // &mut で借りるには mut 宣言が必要
append_world(&mut greeting);
println!("{}", greeting); // "hello, world"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_length() {
let text = String::from("hello");
assert_eq!(calculate_length(&text), 5);
// 借用しただけなので text はテスト内でも使い続けられる
assert_eq!(text, "hello");
}
#[test]
fn test_append_world() {
let mut greeting = String::from("hello");
append_world(&mut greeting);
assert_eq!(greeting, "hello, world");
}
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
hello is 5 bytes
---
hello, world
$ cargo test
running 2 tests
test tests::test_append_world ... ok
test tests::test_calculate_length ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
🪜 Step-by-Step 解説
&textで不変借用を渡すlet len = calculate_length(&text);
&textはtextが指すString本体への参照を作ります。calculate_lengthの引数sの型は&Stringなので、この参照を受け取ります。関数が終わってsがスコープを抜けても、参照が指していた本体(text)は破棄されません。あくまで一時的に「見せてもらった」だけだからです。
textが使える理由println!("{} is {} bytes", text, len);
calculate_length(&text)はtextの所有権を一切動かしていません。動いたのは「textを指すポインタ」だけです。そのため、関数呼び出しの後もtextは変わらず有効な所有者として存在し続け、println!でそのまま使えます。
&mut greetingで可変借用を渡すlet mut greeting = String::from("hello");
append_world(&mut greeting);
&mut greetingは「greetingを書き換えてよい権利」をappend_worldに一時的に貸し出します。関数内のs.push_str(", world")は、この参照を通じてgreeting本体を直接書き換えます。呼び出し元の変数自体がlet mutで宣言されていないと、そもそも&mutで借用することができません(コンパイラが「不変変数を可変借用しようとした」というエラーを出します)。
let r1 = &mut s; let r2 = &mut s;がエラーになる理由問題4で予測した通り、同じ変数
sに対して2つの&mutを同時に作ろうとするとコンパイルエラーになります。借用規則「&mut Tは同時に1個しか存在できない」に違反しているためです(詳細は「コンパイルエラーが出た場合」セクション)。
💡 設計思想・なぜこう書くのか
&T」か「1個だけの&mut T」のどちらか一方のみ——は、「読み取り専用アクセスが複数あっても安全だが、書き込み中のアクセスは他の誰とも共存させてはいけない」という、並行プログラミングの基本原則をそのまま型システムに落とし込んだものです。多くの言語(Go・Java・C++など)では、複数のスレッドや関数が同じデータへの参照・ポインタを自由に持てます。書き込みと読み取りが衝突しないようにするのはミューテックスなどの実行時の仕組みや、プログラマの規律に委ねられており、うっかりミスがあればデータ競合というバグが実行時に(しかもタイミング依存で再現しにくい形で)発生します。Arc<Mutex<T>>やSend/Syncトレイトも、この借用規則の延長線上にあります)。🛑 コンパイルエラーが出た場合
問題4のコードです。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ← ここでエラー
println!("{}, {}", r1, r2);
}
読み方:
error[E0499]: 「同じ値を同時に2回以上、可変借用しようとした」ことを示すエラーコードですfirst mutable borrow occurs here:r1が最初の可変借用であることを示していますsecond mutable borrow occurs here:r2を作ろうとした時点で、まだr1が生きている(後でprintln!で使われる)ため、2個目の可変借用が拒否されていますfirst borrow later used here:r1がprintln!の中で実際に使われるコードなので、コンパイラは「r1はまだ有効に生きている」と判断し、その状態でr2を作ることを許しません
修正方法: 同時に2つの可変借用が必要になる設計そのものを避けるのが基本です。多くの場合、r1を使い終わってから(スコープを抜けてから)改めてr2を作れば解決します。
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
println!("{}", r1);
} // r1 のスコープがここで終わる
let r2 = &mut s; // r1 が死んでいるので OK
println!("{}", r2);
}
なお、不変借用&s同士であれば何個でも同時に存在できます。&s1と&mut s2のように別の変数を指している場合も、当然ながら同時に借用できます。エラーになるのは同じ値に対して可変借用が複数、または不変借用と可変借用が同時に生きている場合だけです。
🌐 他言語との比較
| 観点 | Rust | Go | Java | C++ | TypeScript |
|---|---|---|---|---|---|
| 「読むだけ」の参照を渡す | &T。所有権を渡さず一時的に貸すだけで、コンパイラが有効期間を追跡する | ポインタ(*T)やスライス渡しはあるが、読み取り専用であることを型で強制する仕組みはない | 参照渡しがデフォルト(プリミティブ型以外)。読み取り専用の強制はfinal変数程度で限定的 | const T&で読み取り専用参照を表現できるが、const_castで回避可能。強制力はコンパイラの型チェック止まり | 参照渡しがデフォルト。readonlyはコンパイル時チェックのみで、実行時の強制力はない |
| 「書き換えてよい」参照を同時に複数持てるか | 不可能(借用規則で1個までに制限、コンパイルエラーで検出) | 可能。複数のゴルーチンが同じポインタを自由に共有でき、データ競合はプログラマの責任(go run -raceで実行時検出のみ) | 可能。複数スレッドが同じオブジェクト参照を共有でき、synchronized等で自衛する必要がある | 可能。複数のポインタ/参照が同じメモリを指せて、データ競合の防止は完全にプログラマ責任 | シングルスレッド前提のため実行時のデータ競合は基本的に起きないが、非同期処理での状態不整合は別途起こり得る |
| データ競合の検出タイミング | コンパイル時(借用規則違反はE0499等でビルドが失敗する) | 実行時(-raceフラグ使用時のみ、かつ実行パス依存で見逃しあり) | 実行時(デバッガや障害調査で発覚することが多い) | 実行時、または未定義動作としてサイレントに壊れる | 該当なし(シングルスレッドモデルのため) |
Go・Java・C++はいずれも「複数の書き込み可能な参照を同時に持てる」ため、データ競合の防止はプログラマの規律とツール(レースディテクタ・ロック)に依存します。Rustは言語仕様として「複数の&mutを同時に存在させること自体ができない」ため、単一スレッドのコードであっても借用規則が一貫して適用され、後にマルチスレッド化する際も同じ規則がそのまま安全性を保証します。
🏆 実務での使いどころ
- 関数への軽量な値渡し:
StringやVec<T>のような所有権付きの大きな値を関数に渡すとき、関数側が値を変更・保持する必要がなければ&Tで渡すのが基本です。所有権のムーブやクローンのコストを避けられます - その場での更新処理:
fn normalize(v: &mut Vec<i32>)のように、呼び出し元のデータをその場で書き換える関数(ソート・正規化・バリデーション後の修正など)は&mut Tで受け取るのが定石です - 借用チェッカーがAPI設計を導く: 「この関数は読むだけでいいのか、書き換える必要があるのか」を
&Tと&mut Tの選択によって明示することになるため、関数シグネチャを見るだけで副作用の有無が分かる設計になります - 並行処理への布石: Phase 3で学ぶ
Arc<Mutex<T>>やRwLock<T>は、まさにこの「共有読み取り or 排他書き込み」という借用規則をスレッドをまたいで実現する仕組みです。ここで学ぶ規則がそのままマルチスレッド設計の基礎になります
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
&を付ければ常にムーブを回避できると思い、可変借用でも&だけで済ませようとする | 「参照=借用」という理解が曖昧なため、&と&mutの違いを意識せずに使ってしまう | 値を書き換える必要があるなら&mut Tが必須。&T(不変借用)ではpush_strのような変更操作は呼び出せない(コンパイルエラーになる) |
&mutで借用しようとしたら、借用元の変数にmutを付け忘れてエラーになる | Day 001で学んだlet mutの可変性と、借用時の&mutが別々の概念だと気づきにくい | &mutで貸し出すには、貸す側の変数自体がlet mutで宣言されている必要がある。変数が不変(デフォルト)なら、そもそも書き換え権限を持たないので貸しようがない |
| 借用規則は「同時に2つの変数を扱ってはいけない」という意味だと誤解する | エラーメッセージが「2回目の借用」に言及するため、変数の個数の制約だと早合点しやすい | 制約は「同一の値に対する可変参照が同時に複数存在してはいけない」こと。別々の値であれば&mut aと&mut bを同時に持つのは全く問題ない |
🚀 次のステップ
- 発展: 1つの関数内で「不変借用が生きている間は可変借用を作れない」というケースも試してみてください(例:
let r1 = &s; let r2 = &mut s;)。エラーコードはE0502になります。今日のE0499(可変×可変)との文言の違いを比較すると理解が深まります - 次回予告: Day 008ではスライス型(
&str・&[T])へ進みます。今日学んだ「借用」の考え方が、String全体ではなく一部分だけを指す参照としてどう応用されるかを見ていきます