Day 1: 所有権マスター - 解説

所有権システムの本質

なぜRustは所有権を選んだのか

Rustの目標は「安全性」と「パフォーマンス」の両立です。伝統的に、この2つは トレードオフの関係にありました。

言語 メモリ安全 高パフォーマンス 予測可能性 ランタイムコスト
C/C++ x o o なし
Java/Go o x x GC(5-15%)
Rust o o o なし

所有権システムにより、コンパイル時にメモリ安全性を保証できます。これは、実行時のオーバーヘッドがゼロであることを意味します。

設計原則

Rustの所有権システムは、3つの重要な設計原則に基づいています:

1. RAII (Resource Acquisition Is Initialization)

C++から借りた概念で、リソースの取得と解放をスコープに紐づけます。

{
    let file = File::open("data.txt")?;  // リソース取得
    // ファイルを使った処理
} // スコープを抜ける → 自動的にファイルがクローズされる

利点:

  • リソースリークの防止
  • 例外安全性
  • コードが読みやすい(いつ解放されるかが明確)

2. Zero-cost Abstractions(ゼロコスト抽象化)

高レベルな抽象化が、低レベルなコードと同等のパフォーマンスを持つ。

// 高レベル: イテレータチェーン
let sum: i32 = vec![1, 2, 3, 4, 5]
    .iter()
    .map(|x| x * 2)
    .filter(|x| x > &5)
    .sum();

// コンパイル後は、手書きのループと同じ機械語に
// 実行時オーバーヘッドなし

3. Fearless Concurrency(恐れのない並行性)

所有権システムにより、データ競合がコンパイル時に防がれる。

use std::thread;

let data = vec![1, 2, 3];

// このコードはコンパイルエラー
// thread::spawn(|| {
//     println!("{:?}", data);  // dataの所有権が不明確
// });

// 正しい方法: 所有権を移動
thread::spawn(move || {
    println!("{:?}", data);  // 所有権が明確
});

所有権の深い理解

スタックとヒープ

メモリは大きく2つの領域に分かれます。所有権を理解するには、この違いを知る必要があります。

スタック

特徴:

  • LIFO (Last In, First Out)
  • 固定サイズのデータ
  • 高速(メモリアドレスの計算が簡単)
  • 関数のローカル変数

スタックのメモリレイアウト:
+------------------+  <- スタックポインタ(SP)
| main のローカル   |
+------------------+
| func1 のローカル  |
+------------------+
| func2 のローカル  |  <- 関数呼び出しで積まれる
+------------------+

スタックに格納される型:

let x: i32 = 5;           // 4バイト、スタック
let y: f64 = 3.14;        // 8バイト、スタック
let point = (10, 20);     // 8バイト、スタック
let array = [1, 2, 3];    // 12バイト、スタック

ヒープ

特徴:

  • 動的サイズのデータ
  • 遅い(メモリアロケータが空き領域を探す)
  • プログラムの実行中にサイズが変わる
  • 明示的な管理が必要(RustではOwnershipで自動化)

ヒープのメモリレイアウト:
+------------------+
| 空き領域          |
+------------------+
| String: "hello"  |  <- アロケータが確保
+------------------+
| Vec: [1,2,3,4,5] |
+------------------+
| 空き領域          |
+------------------+

ヒープに格納される型:

let s: String = String::from("hello");  // ヒープ
let v: Vec<i32> = vec![1, 2, 3];        // ヒープ
let b: Box<i32> = Box::new(5);          // ヒープ

スタックとヒープの複合型

let s = String::from("hello");

メモリレイアウト:

スタック(固定サイズ部分):
+----------+
| ptr      | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |
| cap: 5   |
+----------+

- ptr: ヒープへのポインタ(8バイト、64bitシステム)
- len: 現在の長さ(8バイト)
- cap: 確保済み容量(8バイト)
合計: 24バイト(スタック)+ 5バイト(ヒープ)

Move の仕組み

Moveが起こるとき

let s1 = String::from("hello");
let s2 = s1;  // Move発生

Move前:

スタック:
+----------+
| s1: ptr  | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |
| cap: 5   |
+----------+

Move後:

スタック:
+----------+
| s1: 無効 | (コンパイラが使用を禁止)
+----------+
| s2: ptr  | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |        (同じヒープ領域を指す)
| cap: 5   |
+----------+

なぜMoveが必要か

もしMoveがなかったら:

// もしもMoveがなかったら...(疑似コード)
let s1 = String::from("hello");
let s2 = s1;  // s1とs2が同じヒープを指す

// スコープを抜けるとき
// s1が破棄 → ヒープが解放される
// s2が破棄 → 既に解放済みのヒープを再度解放(Double Free!)

Double Freeの危険性:

  • プログラムクラッシュ
  • セキュリティ脆弱性(任意のコード実行)
  • 予測不能な動作

Rustの解決策は、所有者を1つに限定することです。

Copy の仕組み

Copyが起こるとき

let x = 5;
let y = x;  // Copy発生(xはまだ有効)

メモリレイアウト:

スタック:
+----------+
| x: 5     |  (4バイト)
+----------+
| y: 5     |  (4バイト、独立したコピー)
+----------+

なぜCopyが安全か

  • スタック上の固定サイズデータのみ
  • ビット単位のコピーが安価
  • ヒープへのポインタを含まない
  • 二重解放の問題が起こらない

Copy可能な型の判定方法

// Copy可能: すべてのフィールドがCopy
#[derive(Copy, Clone)]
struct Point {
    x: i32,  // i32はCopy
    y: i32,  // i32はCopy
}

// Copy不可能: 1つでもCopyでないフィールドがある
struct Rectangle {
    top_left: Point,      // PointはCopy
    label: String,        // StringはCopyでない!
}

メンタルモデル:所有権の直感的理解

所有権 = 責任

所有権を「責任」として考えると理解しやすいです。

実世界のアナロジー

本の貸し借り:

// あなたが本を持っている
let book = String::from("Rust Programming");

// 友人に本を渡す(所有権の移動)
let friend_has_book = book;

// あなたはもう本を読めない
// println!("{}", book);  // エラー!本はもうない

// 友人だけが本を読める
println!("{}", friend_has_book);

本のコピー:

// 雑誌を持っている(軽い)
let magazine = 5;  // ページ番号として想像

// 誰かにページ番号を教える(コピー)
let someone_else = magazine;

// 両方が独立してページ番号を知っている
println!("私: {}, 相手: {}", magazine, someone_else);

Move vs Borrow vs Clone

操作 構文 元の値 新しい値 メモリコスト
Move `let b = a;` 使えない 使える なし
Borrow `let b = &a;` 使える 読み取り専用 なし
Clone `let b = a.clone();` 使える 使える あり(ヒープコピー)

決定木:どれを使うべきか

データを使いたい
  |
  +-- 元の値も後で使う?
      |
      Yes --> 値を変更する?
      |       |
      |       Yes --> 可変借用 (&mut)
      |       No  --> 不変借用 (&)
      |
      No --> 値を変更する?
             |
             Yes --> Move (所有権移動)
             No  --> Move または Borrow

ビジュアル図解:メモリレイアウト

例1: Stringのライフサイクル

fn main() {
    let s = String::from("hello");  // (1) 作成
    do_something(s);                // (2) Move
    // (4) 関数終了、sは存在しない
}

fn do_something(text: String) {     // (3) 受け取る
    println!("{}", text);
    // (4) textが破棄される(ヒープも解放)
}

メモリの変化:

(1) main で作成:
スタック (main):
+----------+
| s: ptr   | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |
| cap: 5   |
+----------+

(2) 関数呼び出し:
スタック (main):
+----------+
| s: 無効  |
+----------+
スタック (do_something):
+----------+
| text:ptr | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |        (同じヒープ領域)
| cap: 5   |
+----------+

(3) 関数終了:
ヒープが解放される
すべてのスタックフレームがクリア

例2: Vecの成長

let mut v = Vec::new();  // (1) 空のVec
v.push(1);               // (2) 1つ追加
v.push(2);               // (3) 2つ目追加
v.push(3);               // (4) 3つ目追加
// 容量が足りなくなると自動的に再割り当て

メモリの変化:

(1) 空のVec:
スタック:
+----------+
| v: ptr   | -> null
| len: 0   |
| cap: 0   |
+----------+

(2) push(1):
スタック:
+----------+
| v: ptr   | -----> ヒープ: [1][未使用][未使用][未使用]
| len: 1   |        (容量4で確保)
| cap: 4   |
+----------+

(3) push(2):
スタック:
+----------+
| v: ptr   | -----> ヒープ: [1][2][未使用][未使用]
| len: 2   |
| cap: 4   |
+----------+

(4) capacity超過時(例: 5個目追加):
古いヒープ: [1][2][3][4] (破棄される)
                ↓
新しいヒープ: [1][2][3][4][5][未][未][未] (容量8で再確保)

よくある間違いとその理由

1. 所有権移動後の使用

let s = String::from("hello");
let t = s;
println!("{}", s);  // エラー!

エラーメッセージ:

error[E0382]: borrow of moved value: `s`
 --> src/main.rs:4:20
  |
2 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let t = s;
  |             - value moved here
4 |     println!("{}", s);
  |                    ^ value borrowed here after move

なぜエラーか:

  • sの所有権がtに移動
  • sはもう有効な値を指していない
  • 無効なメモリへのアクセスを防ぐため

2. 関数に渡した後の使用

fn consume(s: String) {
    println!("{}", s);
}

let s = String::from("hello");
consume(s);
println!("{}", s);  // エラー!

なぜエラーか:

  • consume(s) で所有権が関数に移動
  • 関数終了時に s が破棄される
  • mainの s はもう無効

3. 部分的なMove

struct Person {
    name: String,
    age: u32,
}

let person = Person {
    name: String::from("Alice"),
    age: 30,
};

let name = person.name;  // nameだけMove
println!("{}", person.age);  // OK: ageはCopy
// println!("{}", person.name);  // エラー!nameは移動済み

ルール: 構造体の一部がMoveされると、構造体全体としては使えなくなる。

所有権のベストプラクティス

1. 借用を優先する

// ❌ 所有権を取るのは重い
fn process_data(data: Vec<i32>) -> Vec<i32> {
    // 処理
    data  // 返す必要がある
}

// ✅ 借用で十分な場合が多い
fn process_data(data: &[i32]) -> Vec<i32> {
    // 処理
    data.iter().map(|&x| x * 2).collect()
}

理由:

  • 呼び出し側が柔軟に使える
  • 関数の責任範囲が明確
  • 意図しない変更を防ぐ

2. clone() は最後の手段

// ❌ 安易なclone
fn get_config() -> Config {
    GLOBAL_CONFIG.clone()  // 毎回コピー
}

// ✅ 参照を返す
fn get_config() -> &'static Config {
    &GLOBAL_CONFIG  // コピーなし
}

// ✅ または Arc で共有
fn get_config() -> Arc<Config> {
    Arc::clone(&GLOBAL_CONFIG)  // 参照カウントのみ
}

3. 小さい型はCopyを実装

// ✅ 小さい構造体はCopyに
#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

// ✅ IDやハンドルもCopyに
#[derive(Copy, Clone)]
struct UserId(u64);

条件:

  • 16バイト以下(目安)
  • ヒープを使わない
  • Copyが自然に感じられる型
  • 4. 関数設計の指針

    // 読み取り専用 → 不変参照
    fn calculate_total(items: &[Item]) -> f64 { }
    
    // 変更が必要 → 可変参照
    fn sort_items(items: &mut [Item]) { }
    
    // 所有権が必要 → 値を取る
    fn consume_and_save(data: Vec<u8>) -> Result<(), Error> { }
    
    // 所有権を返す → ビルダーパターン
    fn add_header(mut request: Request, key: &str, value: &str) -> Request {
        request.headers.insert(key, value);
        request
    }
    

    パフォーマンスへの影響

    ベンチマーク: Clone vs Borrow

    use std::time::Instant;
    
    fn benchmark() {
        let data = "a".repeat(1_000_000);  // 1MBの文字列
    
        // Clone: ~5ms
        let start = Instant::now();
        let cloned = data.clone();
        println!("Clone: {:?}", start.elapsed());
    
        // Borrow: ~0.001ms
        let start = Instant::now();
        let borrowed = &data;
        println!("Borrow: {:?}", start.elapsed());
    }
    

    メモリ使用量

    use std::mem;
    
    fn memory_usage() {
        let s = String::from("hello");
    
        // Stringのサイズ(スタック部分)
        println!("String size: {}", mem::size_of_val(&s));  // 24バイト
    
        // 参照のサイズ
        let r = &s;
        println!("&String size: {}", mem::size_of_val(&r));  // 8バイト
    
        // ヒープの実際のデータ
        println!("Heap size: {}", s.len());  // 5バイト
    }
    

    セルフチェック質問

    レベル1: 基礎

  • 所有権の3つのルールを述べよ
- 各値は所有者を持つ - 所有者は同時に1つだけ - 所有者がスコープを抜けると値は破棄

  • CopyとMoveの違いは?
- Copy: スタック上のビット単位コピー、元の値も使える - Move: 所有権の移動、元の値は使えない

  • clone()とCopyの違いは?
- clone(): 明示的な深いコピー(ヒープも含む) - Copy: 暗黙的な浅いコピー(スタックのみ)

レベル2: 応用

  • このコードはコンパイルできるか?
let s1 = String::from("hello");
let s2 = s1;
let s3 = s1;
  • 答え: No。s2への代入でs1が無効になるため、s3への代入でエラー。
  • このコードはコンパイルできるか?
let x = vec![1, 2, 3];
let y = x;
println!("{}", x.len());
  • 答え: No。yへの代入でxが無効になるため、x.len()でエラー。
  • このコードはコンパイルできるか?
let x = 5;
let y = x;
println!("{}", x);
  • 答え: Yes。i32はCopyなので、xは有効。
  • レベル3: 設計

  • 関数シグネチャを設計せよ
- データを読むだけ: fn read_data(data: &[u8]) -> usize - データを変更: fn modify_data(data: &mut Vec) - データを消費: fn process_data(data: Vec) -> Result<(), Error>

  • 構造体を設計せよ
// 小さい構造体: Copy
#[derive(Copy, Clone)]
struct Point { x: i32, y: i32 }

// 大きい構造体: Cloneのみ
#[derive(Clone)]
struct Config { host: String, port: u16 }

次のステップ

Day 2では「借用」を学びます。所有権を持たずにデータにアクセスする方法を習得しましょう。

学習ポイント:

  • 不変借用(&T
  • 可変借用(&mut T
  • 借用のルール(排他制御)
  • ライフタイムの基礎
  • 借用チェッカーのエラー解決

所有権をマスターしたあなたは、Rustの最大の山を越えました。借用はその延長線上にあり、より柔軟なメモリ管理を可能にします。