所有権(Ownership)
Rust最重要の概念です。これを理解すればRustの大部分が理解できます。
所有権とは
Rustでは、すべての値に「所有者」(owner)がいます。所有者はその値に対する責任を持ちます。
所有権の3つのルール
┌──────────────────────────────────────────────────┐
│ 所有権の3つのルール │
├──────────────────────────────────────────────────┤
│ 1. Rustの各値は「所有者」と呼ばれる変数を持つ │
│ 2. 所有者は同時に1つだけ存在できる │
│ 3. 所有者がスコープを抜けると、値は破棄される │
└──────────────────────────────────────────────────┘
この3つのルールがRustのメモリ安全性を支えています。
ルール1: 各値には所有者がいる
fn main() {
let s = String::from("hello"); // sが"hello"の所有者
println!("{}", s);
}
sが"hello"という文字列の所有者です。
ルール2: 所有者は同時に1つだけ
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権がs1からs2に移動(ムーブ)
// println!("{}", s1); // エラー!s1はもう所有者ではない
println!("{}", s2); // OK
}
ムーブの図解
s1 = String::from("hello");
スタック ヒープ
┌─────────────┐ ┌───────────┐
│ s1 │ │ │
│ ptr ─────────────────────→│ "hello" │
│ len: 5 │ │ │
│ cap: 5 │ └───────────┘
└─────────────┘
s2 = s1; // ムーブ後
┌─────────────┐ ┌───────────┐
│ s1 (無効) │ │ │
│ ptr ────X │ │ "hello" │
│ len: 5 │ │ │
│ cap: 5 │ └───────────┘
└─────────────┘ ↑
┌─────────────┐ │
│ s2 │ │
│ ptr ─────────────────────────────
│ len: 5 │
│ cap: 5 │
└─────────────┘
ルール3: スコープを抜けると値は破棄
fn main() {
{
let s = String::from("hello");
println!("{}", s);
} // ← ここでsがスコープを抜ける。メモリが解放される。
// println!("{}", s); // エラー!sは存在しない
}
なぜこれが重要なのか
C言語などでは、メモリの解放を忘れるとメモリリークが起きます。 逆に、同じメモリを2回解放すると二重解放バグが起きます。
Rustは所有権ルールにより、これらをコンパイル時に防ぎます。
コピーとムーブ
コピーされる型(Copy型)
スタックに収まる小さな型は、代入時にコピーされます。
fn main() {
let x = 5;
let y = x; // コピー
println!("x = {}, y = {}", x, y); // 両方使える
}
Copy型の例: i32, f64, bool, char, タプル(全要素がCopyの場合)
ムーブされる型
ヒープを使う型は、代入時にムーブ(所有権の移動)されます。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ムーブ
// println!("{}", s1); // エラー!
println!("{}", s2);
}
ムーブされる型の例: String, Vec<T>, Box<T>
clone: 明示的なコピー
ヒープのデータもコピーしたい場合は、cloneを使います。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープのデータもコピー
println!("s1 = {}, s2 = {}", s1, s2); // 両方使える
}
clone後のメモリ:
スタック ヒープ
┌─────────────┐ ┌───────────┐
│ s1 │ │ "hello" │ ← s1用
│ ptr ─────────────────────→│ │
└─────────────┘ └───────────┘
┌─────────────┐ ┌───────────┐
│ s2 │ │ "hello" │ ← s2用(別の場所)
│ ptr ─────────────────────→│ │
└─────────────┘ └───────────┘
注意: cloneはヒープのデータをコピーするので、大きなデータでは遅くなります。
関数と所有権
関数に渡すとムーブする
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(); // 所有権をもらう
println!("{}", s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2を渡して、新しい所有権をもらう
// println!("{}", s2); // エラー!
println!("{}", s3); // OK
}
fn gives_ownership() -> String {
String::from("hello")
}
fn takes_and_gives_back(s: String) -> String {
s
}
所有権のまとめ図
┌────────────────────────────────────────────────────────┐
│ 所有権の移動パターン │
├────────────────────────────────────────────────────────┤
│ │
│ 変数代入: let s2 = s1; → s1からs2にムーブ │
│ │
│ 関数呼出: func(s); → sから引数にムーブ │
│ │
│ 関数戻値: let s = func(); → 関数からsにムーブ │
│ │
│ clone: let s2 = s1.clone(); → コピー(両方使える) │
│ │
└────────────────────────────────────────────────────────┘
よくあるエラーと対処
エラー: use of moved value
fn main() {
let s = String::from("hello");
let s2 = s;
println!("{}", s); // error: use of moved value: `s`
}
対処法:
cloneを使う:let s2 = s.clone();- 参照を使う(次のドキュメントで学習):
let s2 = &s;
まとめ
| ルール | 内容 |
|---|---|
| ルール1 | 各値には所有者がいる |
| ルール2 | 所有者は同時に1つだけ |
| ルール3 | スコープを抜けると値は破棄 |
| 操作 | Copy型 | ムーブ型 |
|---|---|---|
| 代入 | コピー | ムーブ |
| 関数に渡す | コピー | ムーブ |
| 両方使いたい | そのまま | clone |
確認テスト
Q1. 所有権のルールとして正しいものはどれですか?
Q2. 以下のうち、Copy型(代入時にコピーされる型)はどれですか?
i32などの整数型、f64などの浮動小数点型、bool、charはCopy型で、代入時にコピーされます。String、Vec、Boxはヒープを使うため、ムーブされます。
Q3. 以下のコードはコンパイルできますか?let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);
let s2 = s1;で所有権がs1からs2にムーブしたため、s1は無効になっています。無効な変数を使おうとしているのでエラーになります。エラーメッセージ: error[E0382]: borrow of moved value: 's1'
Q4. 関数にStringを渡した後も元の変数を使いたい場合、最も効率的な方法はどれですか?
clone()はヒープデータをコピーするためコストがかかり、所有権を返す方法は関数のシグネチャが複雑になります。参照を使えばコピーなしでデータにアクセスでき、所有権も保持できます。
Q5. clone()メソッドについて正しい説明はどれですか?
clone()はヒープ上のデータも含めて深くコピーします。これにより、元の変数と新しい変数の両方が独立したデータを持ちます。ただし、大きなデータでは処理コストがかかります。
次のドキュメント: 03_references_borrowing.md