04. 所有権: Rust 最大の特徴
🎯 この章のゴール
- 所有権の 3 ルールを言えるようになる
- ムーブ・クローン・コピー の違いを理解する
💡 ひとことで言うと
値は 一冊の本、変数は 持ち主。本を誰かに渡したら、自分の手元にはもうありません。持ち主が部屋(スコープ)を出るとき、本は片付けられます。
なぜ所有権?
メモリ管理の方式はだいたい 2 つでした。
- 手動管理(C/C++): 速いが、解放忘れ・二重解放のバグが起きやすい
- GC(Java, Go など): 安全だが、実行時のコストがかかる
Rust はどちらとも違い、コンパイル時に所有権ルールをチェックすることで、GC なしで安全性を保証します。
3 つのルール
- 各値には 所有者 と呼ばれる変数がある
- 所有者は 同時に 1 つだけ
- 所有者がスコープを抜けると、値は 破棄(drop) される
fn main() {
{
let s = String::from("hello"); // s が所有者
println!("{s}");
} // ← ここで s がスコープを抜け、メモリが解放される
}ムーブ: 所有権の移動
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s2 へムーブ
println!("{s1}"); // error[E0382]: borrow of moved value: `s1`
}String はヒープ上にデータを持ちます。もし s1 と s2 の両方が同じデータを所有したら、スコープ終了時に 二重解放 が起きてしまいます。だから Rust は s1 を無効にするのです。
スタック ヒープ
s1 (無効) ─┐
├──▶ [h][e][l][l][o]
s2 ────────┘クローン: 明示的な複製
ヒープのデータごと複製したいときは clone() を呼びます。重い複製は必ず clone() と書くことになるので、どこでコストがかかるかがコードを読めば分かります。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} {s2}"); // どちらも使える
}コピー: スタックだけの型
整数・浮動小数点・bool・char、それらだけからなるタプルなどは Copy トレイトを持ち、代入しても元の変数が使えます。
fn main() {
let x = 5;
let y = x; // コピー
println!("{x} {y}");
}関数と所有権
関数に渡す・関数から返すのも、ムーブ(またはコピー)です。
fn take(s: String) {
println!("{s}");
} // s はここで drop
fn give() -> String {
String::from("gift") // 所有権を呼び出し元へ返す
}
fn main() {
let a = String::from("hi");
take(a); // a はムーブ済み。以降 a は使えない
let b = give(); // b が新しい所有者
println!("{b}");
}毎回所有権を渡して返してもらうのは面倒ですよね。そこで登場するのが次章の 参照(借用) です。
🧪 やってみよう
下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。
まずはこのまま ▶ 実行 して、エラーメッセージを読んでみましょう。Rust はどこでムーブが起き、どこで使おうとしたかを矢印で示してくれます。読めたら clone() を使って直してみよう。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // TODO: s1 も使えるように直そう
println!("{s1} {s2}");
}💡 解答例を見る
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} {s2}");
}📝 まとめ
- 値の所有者は常に 1 つ。スコープを抜けると自動で解放
- ヒープを持つ型(
String,Vecなど)は代入・関数渡しで ムーブ - 複製したいなら
clone()、Copy型は自動でコピー
✅ 理解度クイズ
fn main() {
let s1 = String::from("hi");
let s2 = s1;
println!("{s1}");
}
let s2 = s1; で所有権が s2 に ムーブ し、s1 は無効になります。s1 を使い続けたいなら s1.clone() で複製します。
fn main() {
let a = 10;
let b = a;
println!("{a} {b}");
}
i32 のようにスタックだけで完結する型は Copy トレイトを実装しており、代入しても コピー されるだけなので a もそのまま使えます。
Rust に GC はありません。所有者がスコープを抜けた瞬間に drop が呼ばれ、メモリが解放されます。
fn take(s: String) {} に take(name) と渡した後、name はどうなる?関数に値を渡すのも代入と同じく ムーブ です。関数が終わると s はスコープを抜けて破棄されます。貸すだけにしたいなら次章の 参照 を使います。