第4章: 所有権入門
学習目標
- 所有権の基本ルールを理解する
- スコープとドロップの仕組みを学ぶ
- ムーブセマンティクスを把握する
- クローンとコピーの違いを知る
---
4.1 所有権とは何か?
4.1.1 メモリ管理の歴史
┌─────────────────────────────────────────────────────────┐
│ プログラミング言語のメモリ管理戦略 │
├─────────────────────────────────────────────────────────┤
│ │
│ 【手動管理】 【自動管理】 │
│ C/C++ Java/Python/Go │
│ │
│ malloc/free Garbage Collection │
│ new/delete │
│ │
│ ✓ 高速 ✓ 安全 │
│ ✓ 予測可能 ✓ 生産性高い │
│ ✗ バグが多い ✗ GCポーズ │
│ ✗ 危険 ✗ メモリオーバーヘッド │
│ │
│ │
│ 【Rustのアプローチ】 │
│ 所有権システム │
│ │
│ ✓ 高速(GCなし) │
│ ✓ 安全(コンパイル時保証) │
│ ✓ 予測可能 │
│ ✗ 学習曲線が急 │
│ │
└─────────────────────────────────────────────────────────┘
4.1.2 所有権の3つのルール
Rustの所有権システムは3つのシンプルなルールに基づいています:
┌─────────────────────────────────────────────────────────┐
│ 所有権の3つの黄金律 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ すべての値には、所有者(owner)が存在する │
│ │
│ 2️⃣ 所有者は同時に1つだけ │
│ │
│ 3️⃣ 所有者がスコープを抜けると、値は破棄される │
│ │
└─────────────────────────────────────────────────────────┘
---
4.2 スコープと所有権
4.2.1 変数のスコープ
fn main() {
// sはまだ有効ではない(宣言前)
let s = "hello"; // sがスコープに入る
// ここから使用可能
// sを使用
println!("{}", s);
} // sがスコープを抜ける → 自動的に破棄
4.2.2 String型とヒープメモリ
スタック vs ヒープ:
┌─────────────────────────────────────────────────────────┐
│ スタック vs ヒープ │
├─────────────────────────────────────────────────────────┤
│ │
│ 【スタック】 【ヒープ】 │
│ │
│ 高速 遅い │
│ 固定サイズ 可変サイズ │
│ 自動管理 手動管理(Rustは自動) │
│ LIFO(後入れ先出し) ランダムアクセス │
│ │
│ 例:整数、bool、固定配列 例:String、Vec、Box │
│ │
└─────────────────────────────────────────────────────────┘
String型の内部構造:
let s = String::from("hello");
┌───────────────┐
│ スタック │
├───────────────┤
│ ptr ────────┼──┐
│ len 5 │ │ ┌─────────────────┐
│ capacity 5 │ │ │ ヒープ │
└───────────────┘ │ ├─────────────────┤
└──►│ h e l l o │
└─────────────────┘
---
4.3 ムーブセマンティクス
4.3.1 所有権の移動(Move)
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2に移動(ムーブ)
println!("{}", s2); // ✅ OK
println!("{}", s1); // ❌ エラー!s1は無効
}
エラーメッセージ:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}", s1);
| ^^ value borrowed here after move
内部で何が起きているか:
ステップ1:s1が作成される
┌───────┐
│ s1 │
│ ptr ──┼──► "hello"
│ len 5 │
└───────┘
ステップ2:s2 = s1(シャローコピー + s1無効化)
┌───────┐
│ s1 │ ← 無効!
│ ××× │
└───────┘
┌───────┐
│ s2 │
│ ptr ──┼──► "hello"
│ len 5 │
└───────┘
なぜムーブが必要?
もしディープコピーだったら:
- 毎回メモリをコピー → 遅い
- 不要なメモリ使用
もしシャローコピーだけだったら:
- 二重解放(double free)の危険
// C++なら問題が発生
let s1 = String::from("hello");
let s2 = s1; // シャローコピー
// スコープを抜ける時、s1とs2の両方が同じメモリを解放しようとする!
Rustの解決:ムーブ(所有権の移動)
- シャローコピー + 元の変数を無効化
- 常に1つの所有者のみ
- 二重解放の問題なし
4.3.2 関数と所有権
値を関数に渡すと所有権が移動:
fn main() {
let s = String::from("hello");
takes_ownership(s); // sの所有権が関数に移動
// println!("{}", s); // ❌ エラー!sは無効
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_stringがスコープを抜ける → メモリ解放
関数からの戻り値で所有権を返す:
fn main() {
let s1 = gives_ownership(); // 所有権を受け取る
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2を渡し、戻り値でs3に
// s1とs3は有効、s2は無効
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 所有権を返す
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 受け取った所有権を返す
}
---
4.4 クローン
4.4.1 明示的なディープコピー
本当にデータをコピーしたい場合は clone() を使用:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ディープコピー
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 両方有効
}
cloneの動作:
┌───────┐ ┌───────┐
│ s1 │ │ s2 │
│ ptr ──┼──► "hello" │ ptr ──┼──► "hello"(コピー)
│ len 5 │ │ len 5 │
└───────┘ └───────┘
cloneは高コスト:
- ヒープメモリの割り当て
- データのコピー
- 明示的に呼ぶことでコストを可視化
let s1 = String::from("very long string...");
let s2 = s1.clone(); // 「ここでコピーが発生している」と明示
---
4.5 Copy トレイト
4.5.1 スタックのみのデータ
整数など、スタックに格納されるデータは自動的にコピーされます:
fn main() {
let x = 5;
let y = x; // コピー(ムーブではない)
println!("x = {}, y = {}", x, y); // ✅ 両方有効
}
なぜ?
整数は固定サイズ → スタックに格納
コピーが高速 → ムーブにする意味がない
4.5.2 Copy トレイトを持つ型
以下の型は Copy トレイトを実装しており、自動的にコピーされます:
// スカラー型
let x: i32 = 5;
let y: u64 = 100;
let z: f64 = 3.14;
let b: bool = true;
let c: char = 'a';
// タプル(すべての要素がCopyの場合)
let t: (i32, i32) = (1, 2);
// 固定長配列(要素がCopyの場合)
let a: [i32; 3] = [1, 2, 3];
Copyトレイトを持たない型:
// String、Vec、Boxなど、ヒープを使用する型
let s = String::from("hello"); // Copyではない → ムーブ
// タプルや配列でも、要素にCopyでない型があればCopyでない
let t = (String::from("hello"), 5); // Copyではない
4.5.3 Copyとムーブの判定
┌─────────────────────────────────────────────────────────┐
│ 型の動作の判定フロー │
├─────────────────────────────────────────────────────────┤
│ │
│ 型はCopyトレイトを実装している? │
│ │ │
│ ├─ Yes → コピー(元の変数も有効) │
│ │ │
│ └─ No → ムーブ(元の変数は無効) │
│ │
└─────────────────────────────────────────────────────────┘
実践例:
fn main() {
// Copyトレイト:コピーされる
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // ✅
// Copyなし:ムーブ
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // ❌ エラー
// 明示的なクローン
let s3 = String::from("world");
let s4 = s3.clone();
println!("s3 = {}, s4 = {}", s3, s4); // ✅
}
---
4.6 所有権と関数
4.6.1 関数の引数
fn main() {
let s = String::from("hello");
takes_ownership(s);
// sはもう使えない
let x = 5;
makes_copy(x);
// xはまだ使える(Copyトレイト)
println!("x = {}", x);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_stringがドロップされる
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integerがドロップされる(でも元の値は影響なし)
4.6.2 戻り値と所有権
fn main() {
let s1 = gives_ownership();
println!("{}", s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
// s2は無効、s3は有効
println!("{}", s3);
}
fn gives_ownership() -> String {
String::from("yours")
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
4.6.3 複数の値を返す(タプル)
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("'{}' の長さは {} です", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // タプルで所有権を返す
}
これは面倒... 次の章で参照を学びます!
---
4.7 実践例
例1:所有権の可視化
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1);
let s2 = s1;
// println!("s1 = {}", s1); // エラー
println!("s2 = {}", s2);
let s3 = s2.clone();
println!("s2 = {}, s3 = {}", s2, s3);
}
例2:関数での所有権
fn main() {
let s = String::from("hello");
let s = append_world(s);
println!("{}", s);
}
fn append_world(mut s: String) -> String {
s.push_str(", world");
s
}
例3:整数とStringの違い
fn main() {
// 整数:Copy
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
// String:Move
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // エラー
println!("{}", s2);
}
---
4.8 よくある間違いとその解決
間違い1:ムーブ後の使用
// ❌ エラー
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ムーブ後の使用
// ✅ 解決1:cloneを使う
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
// ✅ 解決2:参照を使う(次の章で学習)
間違い2:関数に渡した後の使用
// ❌ エラー
let s = String::from("hello");
print_string(s);
println!("{}", s); // sはムーブ済み
fn print_string(s: String) {
println!("{}", s);
}
// ✅ 解決1:所有権を返す
let s = String::from("hello");
let s = print_and_return(s);
println!("{}", s);
fn print_and_return(s: String) -> String {
println!("{}", s);
s
}
// ✅ 解決2:参照を使う(次の章)
間違い3:ループ内でのムーブ
// ❌ エラー
let s = String::from("hello");
for _ in 0..3 {
print_string(s); // 1回目でムーブ、2回目でエラー
}
// ✅ 解決1:cloneする
let s = String::from("hello");
for _ in 0..3 {
print_string(s.clone());
}
// ✅ 解決2:参照を使う(次の章)
---
4.9 まとめ
学んだこと
- 所有権の3つのルール
- ムーブセマンティクス
- Copy トレイト
- 関数と所有権
次のステップ
所有権を理解したら、次は参照と借用を学びます。
- 所有権を移動せずに値を使う方法
- &演算子と借用のルール
- 可変参照と不変参照
- The Rust Programming Language - Chapter 4
- Rust By Example - Ownership
- Visualizing Memory Layout
---