Rustの所有権:メモリ安全性の3つの基本原則
所有権はRustの最も際立った特徴であり、これによりRustはガベージコレクタを必要とせずにメモリの安全性を保証することができる。
「所有権」こそが、Rustと他の言語との最も根本的な違いです。これを理解すれば、Rustの設計の本質が理解できるでしょう。
1. 学習内容
- 所有権の3つの原則:各値には、正確に1人の所有者がいる
- 変数は、スコープ外になると自動的に解放(破棄)されます
- 「move」の意味:所有権の移転
CopyとCloneの特性の違い- スタックとヒープのデータ格納方法
- 関数呼び出しにおける所有権の移転
2. 概念図
flowchart LR
A["Value Creation<br>let s = String::from(...)"] --> B["Owner Binding<br>s Has this value"]
B --> C["Transfer of Ownership move<br>let s2 = s; s Failure"]
C --> D["Out of scope drop<br>} Automatic Memory Release"]
D --> E["Memory Security<br>No Double Release,Non-swinging pointer"]
3. ある図書館の物語
(1) 苦悩:その本は2人に同時に借りられていた
トムは小さな図書館を運営しています。彼はよくある問題に直面しました:
- アリスは『Programming in Rust』を借りて、50ページまで読んだ。
- ボブも同じ本が欲しかったので、トムはまたその本を彼に貸した。
- 結局のところ、アリスは50ページにメモを書き、ボブは100ページにメモを書いた。
- ようやくその本が返却されたとき、書き込みはすべてごちゃ混ぜになっており、誰が書いたものなのか見分けることは不可能だった。
- さらに悪いことに、2人が同時に同じページを編集したため、ページが断片化してしまった。
「もし、一冊の本を一度にたった一人だけが借りられるとしたら……」
(2) Rustの所有権に関するルール
fn main() {
let book = String::from("Rust Programming"); // book is the owner of this book
// let book2 = book; // ❌ If you write it this way,book Ownership was transferred to book2
// println!("{}", book); // ❌ book Can no longer be used
// ✅ The Correct Approach:Only one person can hold ownership at a time.
println!("{} Belongs to Tom The Library", book);
} // book Automatically destroyed here——No manual intervention required free
Rustの所有権のルールは、図書館の「一度に1人まで」という利用規則のようなものです。どの値も、任意の時点で所有者は正確に1人だけです。所有者がスコープ外に出ると、その値は自動的に破棄されます。
4. 所有権の3つの原則
graph TB
A[The Three Principles of Ownership] --> B[Principles1: Each value has exactly one owner.]
A --> C[Principles2: It is destroyed as soon as it leaves its scope.]
A --> D[Principles3: Ownership can be transferred(move)]
B --> E[let s = String::from("hi")]
C --> F[} Automatically call at the end drop]
D --> G[let s2 = s; // s No longer valid]
(1) 原則に関する詳細な説明
| 原則 | 説明 | 類例 |
|---|---|---|
| 単一所有者 | どの時点においても、各値は1人のみが「所有」する | 図書館の本は、一度に1人しか借りることができない |
| スコープ終了 = 破棄 | 変数がスコープ外になったときに自動的に呼び出される drop |
返却期限が過ぎると、本は自動的に返却される |
| 所有権の移転(移動) | 代入やパラメータの受け渡し時に所有権が移転し、元の変数は無効になる | 本が人から人へと手渡される |
5. スタックとヒープ
所有権を理解するには、まずスタックとヒープの違いを理解する必要があります:
| ディメンション | スタック | ヒープ |
|---|---|---|
| 割り当て速度 | 非常に高速(push/pop) | やや遅い(空きメモリの検索が必要) |
| データ格納 | コンパイル時にサイズが既知のデータ | サイズが不明または動的に変化するデータ |
| 代表的なタイプ | i32,bool,f64,Array |
String,Vec,Box |
| メモリ管理 | 自動(関数呼び出しスタックフレーム) | 手動による管理または所有権ベースの管理が必要 |
(3) コピー・クローン・移動の比較
| 演算 | 構文 | 元の変数は有効か? | パフォーマンスのオーバーヘッド | 適用可能な型 |
|---|---|---|---|---|
| コピー | let b = a; |
有効 | 非常に低い(スタックのビット単位のコピー) | i32、f64、bool、char、タプル(Copy型を含む) |
| クローン | let b = a.clone(); |
有効 | 高 (スタックメモリの割り当て) | String, Vec<T>, Box<T> |
| 移動 | let b = a; |
有効期限 | 0(ポインタのみをコピーする) | String, Vec<T>, Box<T> |
型が
Copyを実装している場合、代入によって自動コピーが行われます。一方、Copyを実装していない場合、代入によって自動移動が行われます。元の変数を保持する必要がある場合は、明示的な深部コピーを行うために.clone()を使用してください。
fn main() {
// Data on the Stack:Fixed size,Copy Semantics
let x: i32 = 5; // Allocation on the Stack 4 Byte
let y = x; // Make a copy, x and y are both valid.
println!("x={}, y={}", x, y); // ✅ All are acceptable
// Load the data:Varies in size,move Semantics
let s1 = String::from("hello"); // s1 On the stack(ptr/len/cap),The actual data is on the heap.
let s2 = s1; // ❌ Ownership from s1 Transfer to s2
// println!("{}", s1); // ❌ Compilation Error:s1 Has been moved
println!("{}", s2); // ✅ s2 It's the new owner.
}
6. 所有権の例
(1) ▶ サンプル:「move」のセマンティクス――所有権の移転(難易度 ⭐⭐)
// ============================================
// Demonstration of Ownership Transfer:Occurs during assignment move
// ============================================
fn main() {
let s1 = String::from("Rust");
let s2 = s1; // s1 Ownership was transferred to s2
// println!("s1: {}", s1); // ❌ Compilation Error!s1 Has been moved
println!("s2: {}", s2); // ✅ s2 I am the owner now
// Integer types are Copy, So it won't move
let a = 42;
let b = a; // a The value is copied to b
println!("a: {}, b: {}", a, b); // ✅ Both are effective.
}
出力:
s2: Rust
a: 42, b: 42
整数などのスカラー型は
Copyトレイトを実装しているため、代入時には移動ではなくコピーが行われます。一方、StringはCopyを実装していないため、代入時には移動が行われます。移動後、元の変数は無効になります。これは、「ダブルフリー」を防ぐための Rust の重要な設計上の特徴です。
(2) ▶ サンプル:関数呼び出しにおける所有権の引き渡し (難易度 ⭐⭐)
// ============================================
// Transfer of Ownership in Parameter Passing and Return Values
// ============================================
fn take_ownership(s: String) {
println!("Acquire ownership: {}", s);
} // s Destroyed here(drop)
fn give_ownership() -> String {
let s = String::from("Newly created string");
s // Ownership is returned to the caller
}
fn main() {
let s1 = String::from("hello");
take_ownership(s1); // s1 Ownership was transferred to the function
// println!("{}", s1); // ❌ s1 Expired
let s2 = give_ownership(); // Acquiring Ownership from a Function
println!("s2: {}", s2); // ✅ s2 Ownership
// Send it in and then send it back
let s3 = String::from("Passing back and forth");
let s3_back = takes_and_returns(s3);
// println!("{}", s3); // ❌ s3 Has been moved
println!("s3_back: {}", s3_back); // ✅
}
fn takes_and_returns(s: String) -> String {
println!("Inside a function: {}", s);
s // Return ownership to the caller
}
出力:
Acquire ownership: hello
s2: Newly created string
Inside a function: Passing back and forth
s3_back: Passing back and forth
関数が呼び出されると、渡された引数の所有権は関数に移ります。関数が値を返すと、所有権は呼び出し元に返されます。これが「関数間で所有権が流れる」という意味であり、「鍵を持っている者がドアを開けることができる」という原則と同じです。
(3) ▶ サンプル:「コピー」と「クローン」の比較(難易度:⭐⭐⭐)
// ============================================
// Copy(Auto-Copy) vs Clone(Explicit Cloning)
// ============================================
fn main() {
// --- Copy Type:Automatic Copy on Assignment ---
let num1 = 100;
let num2 = num1; // Auto-Copy,num1 Still valid
println!("Copy: num1={}, num2={}", num1, num2); // ✅
// --- Clone Type:Must be explicitly called .clone() ---
let s1 = String::from("Cloning required");
let s2 = s1.clone(); // Explicit Cloning,s1 Still valid
println!("Clone: s1={}, s2={}", s1, s2); // ✅
// --- Neither Clone Nor Copy: only move ---
let v1 = vec![1, 2, 3];
let v2 = v1; // move!v1 Failure
// println!("{:?}", v1); // ❌
println!("v2: {:?}", v2); // ✅
}
出力:
Copy: num1=100, num2=100
Clone: s1=Cloning required, s2=Cloning required
v2: [1, 2, 3]
コピー:代入時に自動的にビット単位のコピーを実行します。元の変数は有効なままです(スタック上のデータに適用されます)。クローン:
.clone()メソッドを明示的に呼び出す必要があります。ヒープ上のデータに適用されます。クローンは処理コストが高いため(ヒープメモリの割り当てが必要となる)、Rust では「クローンを行う場合は明示的に選択しなければならない」ように設計されています。
(4) ▶ サンプル:関数内での所有権の取得と返却に関するパターン (難易度 ⭐⭐⭐)
// ============================================
// Common Patterns:Send it in and then send it back(Get+Return)
// ============================================
struct User {
name: String,
score: i32,
}
fn boost_score(mut user: User, bonus: i32) -> User {
user.score += bonus;
user
}
fn format_user(user: &User) -> String {
format!("{}: {} pts", user.name, user.score)
}
fn main() {
let alice = User {
name: String::from("Alice"),
score: 80,
};
println!("Before the upgrade: {}", format_user(&alice));
let alice = boost_score(alice, 15);
println!("After the upgrade: {}", format_user(&alice));
let alice = boost_score(alice, 10);
println!("Upgrade Again: {}", format_user(&alice));
let mut bob = User {
name: String::from("Bob"),
score: 60,
};
let bonus = 20;
bob.score += bonus;
println!("Bob After the bonus points are added: {}", format_user(&bob));
let names = vec![alice.name.clone(), bob.name.clone()];
println!("All users: {:?}", names);
println!("Alice Final Score: {}", alice.score);
println!("Bob Final Score: {}", bob.score);
}
出力:
Before the upgrade: Alice: 80 pts
After the upgrade: Alice: 95 pts
Upgrade Again: Alice: 105 pts
Bob After the bonus points are added: Bob: 80 pts
All users: ["Alice", "Bob"]
Alice Final Score: 105
Bob Final Score: 80
構造体の所有権の譲渡は、基本型の場合と同様の方法で行われます。
boost_score所有権はパラメータを通じて取得され、変更後に返されます。これが「取得・返却」パターンです。参照を使用することで(&User)、所有権を取得することなく読み取り専用のアクセスが可能になります。
(5) ▶ サンプル:所有関係と集合の相互作用(難易度 ⭐⭐⭐)
// ============================================
// Vec,String Common Interaction Patterns with Ownership
// ============================================
fn take_first_word(words: &Vec<String>) -> Option<&str> {
words.first().map(|s| s.as_str())
}
fn remove_and_return(vec: &mut Vec<i32>, index: usize) -> Option<i32> {
if index < vec.len() {
Some(vec.remove(index))
} else {
None
}
}
fn main() {
let mut words = vec![
String::from("hello"),
String::from("rust"),
String::from("world"),
];
println!("Original vector: {:?}", words);
if let Some(first) = take_first_word(&words) {
println!("The First Word: {}", first);
}
let removed = remove_and_return(&mut words, 1);
println!("Remove Index 1: {:?}, Remaining: {:?}", removed, words);
let mut numbers = vec![10, 20, 30, 40, 50];
while let Some(val) = numbers.pop() {
println!("Pop up: {}", val);
}
println!("The vector is empty: {:?}", numbers);
let s1 = String::from("foo");
let s2 = String::from("bar");
let combined = s1 + &s2;
println!("Stitching Results: {}", combined);
let data = vec![String::from("a"), String::from("b")];
for word in &data {
println!("Borrow: {}", word);
}
println!("The vector is still available: {:?}", data);
let data2 = vec![1, 2, 3];
for num in data2 {
println!("Consumption: {}", num);
}
}
出力:
Original vector: ["hello", "rust", "world"]
The First Word: hello
Remove Index 1: Some(20), Remaining: ["hello", "world"]
Pop up: 50
Pop up: 40
Pop up: 30
Pop up: 20
Pop up: 10
The vector is empty: []
Stitching Results: foobar
Borrow: a
Borrow: b
The vector is still available: ["a", "b"]
Consumption: 1
Consumption: 2
Consumption: 3
for item in &vecトラバーサルを借用する(ベクトルはそのまま利用可能のまま)。for item in vecトラバーサルを消費する(ベクトルは移動される)。+この演算子は左側の文字列を消費する。pop()Option<T>を返し、最後の要素を消費する。
❓ よくある質問
move の後、元の変数はどうなるのですか?.clone() と move、どちらのパフォーマンスが優れていますか?move はオーバーヘッドがゼロですが、clone ではヒープメモリの割り当てが必要になります。📖 まとめ
- Rustにおける所有権は、3つの基本原則に基づいています。それは、「単一の所有者」、「スコープ終了時の自動破棄」、そして「譲渡可能な所有権」です。
- スタック上のデータ(スカラー型)はコピーセマンティクスに従います。代入を行うと、値が自動的にコピーされます。
- ヒープ上のデータ(String、Vec)はmoveセマンティクスに従います。代入を行うと、所有権が移転します。
- 再配置後、元の変数は無効となり、コンパイラはその変数へのアクセスをブロックします。これはコンパイル時のチェックです。
- 関数呼び出しや戻り値についても、所有権が移転する
.clone()元の変数を保持する明示的なディープコピー――ただし、パフォーマンスを犠牲にする
📝 練習問題
- 難易度 ⭐:
Stringを作成し、それを別の変数に代入した後、最初の変数を表示しようとするプログラムを作成してください。コンパイラのエラーメッセージを確認してください。 - 難易度 ⭐⭐: 文字列の末尾に「 world」を追加して返す関数
fn append_world(s: String) -> Stringを作成してください。mainでこの関数を呼び出し、関数への引数の受け渡しや関数からの戻り値の受け取りにおいて、所有権がどのように扱われるかを確認してください。 - 難易度 ⭐⭐⭐: 文字列とその長さを返し、かつその文字列の所有権を返す関数
fn calculate_length(s: String) -> (String, usize)を定義してください。関数の呼び出し後も、元の変数が引き続き使用できることを確認してください。



