結論:Rustの所有権が「わからない」と感じるのは、あなたの理解力の問題ではなく、他の言語にはない概念(1つの値の持ち主は常に1人・借りたら返す・借りている期間を追跡する)を、用語の暗記から入っているからです。3つのルール→ムーブ→参照と借用→借用のルール→ライフタイムの順に、実際にエラーを起こしながら1つずつ潰していけば、必ず腹落ちします。
「所有権」「借用」「ライフタイム」という言葉だけを聞くと、いかにも難しそうに見えます。ですが実体はシンプルで、「誰かが必ず解放する」「読む人と書く人が同時にいない」という2つの安全ルールを、コンパイラが実行前にチェックしているだけです。ここでは、つまずきやすい順に1つずつ、具体的なコード例で解消していきます。
なぜ所有権があるのか ― GCなしでメモリを安全にする仕組み
C言語のようにヒープを自分で free する言語では、「解放し忘れ(メモリリーク)」や「二重解放」「解放後に使ってしまう(use after free)」といったバグが起きます。Java や Python はガベージコレクション(GC)でこれを回避しますが、GCには実行時のオーバーヘッド(一時停止など)が伴います。
Rust は第三の道を選びました。GCを使わず、代わりに「所有権(ownership)」というコンパイル時のルールでメモリ安全性を保証します。実行時コストはゼロですが、その分コンパイラのチェックが厳しく、初心者はここで「コンパイルが通らない」という壁にぶつかります。この壁の正体を1段ずつ分解していきます。
所有権の3つのルール ― まずはここだけ暗記する
Rustの所有権は、たった3つのルールに集約されます。
この3つさえ頭に入れば、これから出てくる「ムーブ」「借用」「ライフタイム」は、すべてこの3ルールの具体例に過ぎないと分かります。実際に手を動かして確認したい場合は、所有権の3つのルール をブラウザでそのまま実行してみてください(無料・登録不要、環境構築も不要です)。
ムーブ(move)で詰まる人へ ― なぜ代入しただけでエラーになるのか
Rustで最初に「?」となるのが、次のようなコードです。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s1 から s2 へ「ムーブ」する
println!("{s2}"); // OK
println!("{s1}"); // ← コンパイルエラー! s1 はもう使えない
}
数値(i32 など)なら let b = a; としても a と b は両方使えます。ところが String だと、代入した瞬間に元の変数(s1)が使えなくなります。
理由は String の内部構造にあります。String は「スタックにポインタ・長さ・容量、ヒープに実データ」という形をしていて、let s2 = s1; のとき Rust はスタック上の管理情報だけを s2 にコピーします。ヒープの実データは複製しません(コストが高いからです)。もしこのまま s1 と s2 の両方を有効にしておくと、スコープの終わりで両方が同じヒープを解放しようとし、「二重解放」という致命的なバグになります。
そこでRustは、所有権ルール2(所有者は1人だけ)を守るために、代入した時点でs1を無効化します。これが「ムーブ(move)」です。コピーではなく、所有権そのものが引っ越したと考えます。この状態で s1 を使おうとすると、コンパイラは E0382: borrow of moved value(ムーブ済みの値を借用しようとしている)というエラーで教えてくれます。エラーメッセージの読み方と直し方は E0382: borrow of moved value ― ムーブ後に使った にまとめています。
- ヒープを持つ型(
String、Vecなど)の代入・関数渡しは既定でムーブ。元の変数は無効になる。 - スタック完結型(
i32など)はコピー。元の変数もそのまま使える。
「代入したら元が使えなくなる」のは驚きですが、これは二重解放を書いた瞬間にコンパイラが検出してくれているということです。実際にムーブの挙動と、両方の変数を使いたいときの clone() は ムーブ(move)― 所有権が移る と clone とコピー ― 本当に複製したいとき でブラウザ実行しながら確認できます。関数に値を渡すときも同じルールが働くことは 関数と所有権 ― 引数に渡すとムーブする で扱っています。
参照と借用 ― 「渡す」ではなく「貸す」
ムーブのたびに clone() していては、値をコピーするコストがかかり続けます。そこで使うのが**参照(reference)**です。所有権を渡す(ムーブ)のではなく、「ちょっと見せて、使い終わったら返す」ができれば、ムーブの悩みは消えます。
fn main() {
let s = String::from("hello");
let n = calc_length(&s); // &s =「s を貸す」。所有権はムーブしない
println!("'{s}' の長さは {n}"); // s はまだ使える!
}
fn calc_length(text: &String) -> usize {
text.len()
}
&s は「s への参照」を作るだけで、所有権は渡しません。この行為を**借用(borrowing)と呼びます。関数が終わっても text は借り物なのでヒープを解放せず、所有者である main の s はそのまま生き続けます。既定の & で作る参照は不変(読み取り専用)**で、値を書き換えたいときは &mut を使います。実際にムーブしてエラーになったコードを & に書き換えるだけで解決する体験は 参照と借用 ― & で貸す で、書き換えのための可変参照は 可変参照 ― &mut で書き換える でそれぞれブラウザ実行できます。
借用のルールで詰まったら ― 「読み手は何人でも、書き手は1人だけ」
Rustの借用には明確なルールがあります。ある時点で、次のどちらか一方だけしか持てません。
- 不変参照(
&T)はいくつでも同時に持てる(みんなで読むだけなら安全)。 - 可変参照(
&mut T)はただ1つだけ。しかもそのとき、他の参照(不変も可変も)は1つも持てない。
図書館の本に例えると、みんなで読むのはOKでも、誰かが書き込んでいる最中は他の誰も触れない、というイメージです。次のコードはエラーになります。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不変参照(読み手)
let r2 = &mut s; // ← エラー! 読み手がいるのに書き手を作ろうとした
println!("{r1} {r2}");
}
このルールが必要な理由は、複数の場所から同時に同じデータへアクセスし、そのうち1つでも書き込みがあり、調停する仕組みがない状態=データ競合(data race)を防ぐためです。多くの言語では実行時にたまたま起きるまで気づけませんが、Rustは「書き込む人がいるなら他の誰もアクセスできない」をコンパイル時に保証するので、データ競合はコンパイルを通った時点で存在しません。
- 「可変な借用が同時に2つ」でエラーが出たら、E0499: cannot borrow as mutable more than once ― 可変借用が二重 を参照してください。
- 「不変借用中に書き換えた」でエラーが出たら、E0502: 不変借用中に書き換えた を参照してください。
実際にOKな例・エラーになる例の両方を手元で書き換えながら試せるのが 借用のルール ― データ競合を防ぐ です。
ライフタイムが「難しい」と感じたら
ライフタイムという言葉が一番身構えてしまう原因ですが、正体は「その参照が有効であり続ける期間」を、コンパイラ(借用チェッカー)が追跡しているだけです。次のコードを見てください。
fn main() {
let r;
{
let x = 5;
r = &x; // r は x を借りる
} // ← x がここで死ぬ(スコープを抜ける)
println!("{r}"); // ← エラー! r は死んだ x を指している(ダングリング)
}
r は x を借りているのに、x より長く生きようとしています。コンパイラは「借りた側が、貸した側より長生きしている」ことを検出してエラーにします。これは、借りた家がすでに取り壊されているのに合鍵を使おうとしているような状態です。
つまりライフタイムは、ダングリング参照(すでに無効になったデータへの参照)を防ぐ仕組みを、あらゆる参照に一般化したものです。多くの場合コンパイラが自動で推論してくれるので、普段は意識しません。明示的に書く必要が出てくるのは、コンパイラだけでは参照どうしの関係を判断できない場面だけです。この考え方は ライフタイムとは ― 参照の有効期間 で、明示的な注釈の書き方は ライフタイム注釈 ― 関係をコンパイラに伝える で詳しく扱っています(この2レッスンは概念の読み物です)。「ライフタイムの指定漏れ」でエラーになったときは E0106: missing lifetime specifier ― ライフタイムの指定漏れ を参照してください。
つまずいたら見るエラー逆引き
所有権・借用まわりのコンパイルエラーは、Rustが正しく機能している証拠です。焦らず、次のエラー逆引きで原因と直し方を確認してください。
- E0382: borrow of moved value ― ムーブ後に使った
- E0499: cannot borrow as mutable more than once ― 可変借用が二重
- E0502: 不変借用中に書き換えた
- E0106: missing lifetime specifier ― ライフタイムの指定漏れ
他のエラーも含めた一覧は Rustのエラー逆引き にまとめています。
次に読む
- 所有権の3つのルール ― まずここから。3ルールを声に出して覚えます。
- ムーブ(move)― 所有権が移る と clone とコピー ― 本当に複製したいとき
- 参照と借用 ― & で貸す と 可変参照 ― &mut で書き換える
- 借用のルール ― データ競合を防ぐ
- ライフタイムとは ― 参照の有効期間
- Rustコース トップ ― 全43レッスン。第4〜5章の所有権・借用はほとんどがブラウザでそのまま実行でき、第6章のライフタイムは概念の読み物として整理されています。環境構築なし・登録不要で、いますぐ続きから試せます。