Day 2: 借用チェッカー攻略 - 解説

借用チェッカーの役割

借用チェッカー(Borrow Checker)は、Rustコンパイラの中核機能で、以下をコンパイル時に防ぎます:

  • データ競合(Data Race)
  • ダングリング参照(Dangling Reference)
  • Use After Free
  • Iterator Invalidation

これらはすべて、C/C++では実行時エラーやセキュリティ脆弱性の原因となる問題です。

借用チェッカーの仕組み

// 借用チェッカーが追跡する情報
let mut s = String::from("hello");  // 1. sの所有権が開始

let r1 = &s;                        // 2. r1の不変借用が開始
let r2 = &s;                        // 3. r2の不変借用が開始
println!("{}, {}", r1, r2);         // 4. r1, r2の使用
                                    // 5. r1, r2のライフタイムが終了

let r3 = &mut s;                    // 6. r3の可変借用が開始(安全)
r3.push_str(" world");              // 7. r3を使った変更
println!("{}", r3);                 // 8. r3の使用
                                    // 9. r3のライフタイムが終了

NLL (Non-Lexical Lifetimes)

従来のライフタイム(Rust 2015以前)

// Rust 2015: コンパイルエラー
let mut s = String::from("hello");

let r = &s;
// rのライフタイムはスコープ全体
// (実際には使われていなくても)

s.push_str(" world");  // エラー!rがまだ生きている
println!("{}", r);

問題点: 参照のライフタイムがレキシカルスコープ({})に縛られる。

NLL(Rust 2018以降)

// Rust 2018+: コンパイル成功
let mut s = String::from("hello");

let r = &s;
println!("{}", r);     // rの最後の使用
                       // rのライフタイム終了

s.push_str(" world");  // OK!rはもう使われない

改善点: 参照のライフタイムが「最後に使用された場所」まで。

NLLの詳細な動作

fn demonstrate_nll() {
    let mut data = vec![1, 2, 3];

    // ケース1: 不変借用が続く
    let r1 = &data;
    println!("{:?}", r1);  // r1の使用
    println!("{:?}", r1);  // r1の使用(まだ生きている)
    // r1のライフタイム終了

    // ケース2: 可変借用が始められる
    let r2 = &mut data;    // OK: r1は終了している
    r2.push(4);

    // ケース3: 複雑なケース
    let r3 = &data;
    let r4 = &data;

    // r3とr4を条件分岐で使う
    if r3.len() > 0 {
        println!("{:?}", r3);  // r3の最後の使用
    }

    // r4はまだ生きている
    println!("{:?}", r4);  // r4の最後の使用

    // ここで両方のライフタイムが終了
    data.push(5);  // OK
}

借用の設計原則

読者・書者問題(Readers-Writer Problem)

借用システムは、並行処理の古典的な問題「読者・書者問題」を解決します。

ルール:

  • 複数の読者(不変借用): 同時に読み取りOK
  • 単一の書者(可変借用): 書き込み中は他のアクセス不可

時刻    不変借用     可変借用     状態
------------------------------------------------
t0      -           -           空き
t1      r1          -           読み取り中
t2      r1, r2      -           複数読み取り
t3      r1, r2, r3  -           複数読み取り
t4      -           -           空き(全ての読者が終了)
t5      -           w1          書き込み中
t6      -           -           空き

排他制御の自動化

C++の問題:

std::vector<int> data = {1, 2, 3};
int& first = data[0];  // 参照を取得

data.push_back(4);     // ベクタが再確保される可能性
// first は無効になる(ダングリング参照)

std::cout << first << std::endl;  // 未定義動作!

Rustの解決:

let mut data = vec![1, 2, 3];
let first = &data[0];  // 不変借用

// data.push(4);  // コンパイルエラー!
// 不変借用中は変更できない

println!("{}", first);  // 安全

ライフタイムの基礎

暗黙的なライフタイム

多くの場合、ライフタイムは推論されます。

// ライフタイムは省略されている
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

// 実際のライフタイム(コンパイラが推論)
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

明示的なライフタイム

複数の参照がある場合、ライフタイムを明示する必要があります。

// エラー: ライフタイムが不明
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// 正しい: ライフタイムを明示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

意味: 戻り値のライフタイムは、xとyのうち短い方と同じ。

ライフタイムの実例

fn main() {
    let string1 = String::from("long string");
    let result;

    {
        let string2 = String::from("short");
        result = longest(&string1, &string2);
        println!("長い方: {}", result);  // OK: string2はまだ有効
    }

    // println!("{}", result);  // エラー!string2が破棄された後
}

メモリの状態:

外側のスコープ:
+----------------+
| string1 (有効) |
| result: ?      |
+----------------+
    |
    v
内側のスコープ:
+----------------+
| string2 (有効) |
| result: &str   | -----> string2 または string1 を指す
+----------------+
    |
    v
スコープ終了:
+----------------+
| string1 (有効) |
| result: 無効   | -----> string2 が破棄された
+----------------+

メンタルモデル:借用の直感的理解

図書館のアナロジー

不変借用(読み取り専用):

let book = String::from("Rust Book");
let reader1 = &book;  // 1人目が読む
let reader2 = &book;  // 2人目も読める(コピーして読む)
let reader3 = &book;  // 3人目も読める

// 誰も本を変更できない
// 全員が同じ内容を読める

可変借用(書き込み):

let mut book = String::from("Draft");
let editor = &mut book;  // 編集者が独占

// editor.push_str(" - Edited");  // 編集できる
// 他の人は読めない(編集中のため)
// 編集が終わるまで待つ

所有権 vs 借用の決定木

データを使いたい
  |
  +-- 後で元のデータも使う?
      |
      No --> 所有権を移動(Move)
      |
      Yes --> 値を変更する?
              |
              No --> 不変借用(&T)
              |      - 複数同時OK
              |      - パフォーマンスが良い
              |
              Yes --> 可変借用(&mut T)
                      - 1つだけ
                      - 排他的アクセス

ビジュアル図解:借用のライフタイム

例1: 不変借用の重複

fn example1() {
    let data = vec![1, 2, 3, 4, 5];
    //  ^^^^^^^^ データの所有者

    let r1 = &data;
    //       ^^^^^ r1のライフタイム開始

    let r2 = &data;
    //       ^^^^^ r2のライフタイム開始

    let r3 = &data;
    //       ^^^^^ r3のライフタイム開始

    println!("{:?}, {:?}, {:?}", r1, r2, r3);
    //                            ^^  ^^  ^^ 全て使用

    // ここでr1, r2, r3のライフタイムが終了
}

ライフタイムのタイムライン:

時刻    data    r1      r2      r3
-------------------------------------
t0      作成    -       -       -
t1      有効    開始    -       -
t2      有効    有効    開始    -
t3      有効    有効    有効    開始
t4      有効    有効    有効    有効    <- println!
t5      有効    終了    終了    終了    <- ライフタイム終了
t6      破棄    -       -       -

例2: 可変借用の排他性

fn example2() {
    let mut data = vec![1, 2, 3];
    //  ^^^^^^ 可変な所有者

    let r1 = &mut data;
    //       ^^^^^^^^^ r1の可変借用開始(データを独占)

    r1.push(4);
    //  ^^^^^^^ r1を使った変更

    println!("{:?}", r1);
    //               ^^ r1の最後の使用
    // r1のライフタイム終了

    let r2 = &mut data;
    //       ^^^^^^^^^ r2の可変借用開始(r1は終了済み)

    r2.push(5);

    println!("{:?}", r2);
    // r2のライフタイム終了
}

ライフタイムのタイムライン:

時刻    data      r1          r2
----------------------------------------
t0      [1,2,3]   -           -
t1      [1,2,3]   借用開始    -           <- 排他的
t2      [1,2,3,4] 使用中      -           <- r1.push(4)
t3      [1,2,3,4] 終了        -           <- println!後
t4      [1,2,3,4] -           借用開始    <- 今度はr2が排他的
t5      [1,2,3,4,5] -         使用中      <- r2.push(5)
t6      [1,2,3,4,5] -         終了        <- println!後

よくあるエラーパターンと理由

エラー1: 可変と不変の混在

let mut s = String::from("hello");
let r1 = &s;      // 不変借用
let r2 = &mut s;  // エラー!

println!("{}, {}", r1, r2);

エラーメッセージ:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

理由:

  • r1が不変借用している間、データは変更されないことが保証されている
  • r2が可変借用を作ると、その保証が破られる
  • よって、両立できない

現実世界の例:

読者がドキュメントを読んでいる間に、
編集者が同じドキュメントを編集し始めたら、
読者は混乱してしまう。

Rustはこれを防ぐ。

エラー2: 複数の可変借用

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;  // エラー!

println!("{}, {}", r1, r2);

エラーメッセージ:

error[E0499]: cannot borrow `s` as mutable more than once at a time

理由:

  • 2つの可変借用があると、どちらからでも変更できる
  • データ競合の可能性がある
  • よって、1つだけに制限

データ競合の例:

// もし複数の可変借用が許されたら...
let mut data = vec![1, 2, 3];
let r1 = &mut data;
let r2 = &mut data;

// r1がベクタを再確保
r1.push(4);  // ヒープが再確保される可能性

// r2は古いヒープを指している(ダングリング!)
r2.push(5);  // 未定義動作

エラー3: 借用中の変更

let mut s = String::from("hello");
let r = &s;
s.push_str(" world");  // エラー!
println!("{}", r);

エラーメッセージ:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

理由:

  • rが不変借用している間、sの内容は変わらない保証がある
  • s.push_str()で変更すると、その保証が破られる
  • rが使用される前に変更されるのは危険

借用のベストプラクティス

1. 借用のスコープを最小化

// ❌ 悪い例: 借用が長すぎる
fn bad_example() {
    let mut data = vec![1, 2, 3];
    let r = &data;

    // ... 100行のコード ...
    // rを使わない処理
    // ... さらに100行 ...

    println!("{:?}", r);  // ようやく使う
}

// ✅ 良い例: 借用を最小化
fn good_example() {
    let mut data = vec![1, 2, 3];

    // ... 処理 ...

    {
        let r = &data;
        println!("{:?}", r);  // すぐ使う
    }  // rのスコープが終了

    // dataを自由に変更できる
    data.push(4);
}

2. &String より &str を優先

// ❌ 柔軟性が低い
fn process_string(s: &String) {
    println!("{}", s);
}

// ✅ より汎用的
fn process_str(s: &str) {
    println!("{}", s);
}

fn main() {
    let string = String::from("hello");
    let literal = "world";

    // process_string(&string);  // OK
    // process_string(literal);  // エラー!&strは&Stringではない

    process_str(&string);  // OK: &Stringは&strに自動変換
    process_str(literal);  // OK: &strはそのまま
}

3. 可変借用を不必要に使わない

// ❌ 読むだけなのに可変借用
fn bad_get_length(s: &mut String) -> usize {
    s.len()
}

// ✅ 不変借用で十分
fn good_get_length(s: &str) -> usize {
    s.len()
}

理由:

  • 可変借用は排他的なので、柔軟性が低い
  • 意図が明確になる(読むだけなのか、変更するのか)
  • 並列処理がしやすい
  • 4. 構造体でのライフタイム設計

    // ✅ ライフタイムを明示
    struct Excerpt<'a> {
        text: &'a str,  // textのライフタイムは'a
    }
    
    impl<'a> Excerpt<'a> {
        // selfと同じライフタイム
        fn get_text(&self) -> &'a str {
            self.text
        }
    
        // 新しいライフタイムも可能
        fn first_sentence(&self) -> &str {
            self.text.split('.').next().unwrap_or("")
        }
    }
    
    fn main() {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let excerpt = Excerpt { text: &novel };
    
        println!("抜粋: {}", excerpt.get_text());
    }
    

    パフォーマンスへの影響

    ゼロコスト抽象化

    借用チェックはコンパイル時に行われるため、ランタイムコストはゼロです。

    // このコードは...
    fn sum(data: &[i32]) -> i32 {
        data.iter().sum()
    }
    
    // コンパイル後は生のポインタ操作と同じアセンブリになる
    // ランタイムチェックなし
    

    ベンチマーク: 借用 vs unsafe

    use std::time::Instant;
    
    fn benchmark() {
        let data: Vec<i32> = (0..10_000_000).collect();
    
        // 安全な借用
        let start = Instant::now();
        let sum1 = safe_sum(&data);
        let time1 = start.elapsed();
    
        // unsafeな生ポインタ
        let start = Instant::now();
        let sum2 = unsafe { unsafe_sum(&data) };
        let time2 = start.elapsed();
    
        println!("安全な借用: {:?}", time1);      // ~20ms
        println!("unsafeポインタ: {:?}", time2);  // ~20ms(同じ!)
        assert_eq!(sum1, sum2);
    }
    
    fn safe_sum(data: &[i32]) -> i32 {
        data.iter().sum()
    }
    
    unsafe fn unsafe_sum(data: &[i32]) -> i32 {
        let ptr = data.as_ptr();
        let len = data.len();
        let mut sum = 0;
        for i in 0..len {
            sum += *ptr.add(i);
        }
        sum
    }
    

    結果: パフォーマンスは同じ、安全性は段違い。

    セルフチェック質問

    レベル1: 基礎

  • 不変借用と可変借用の違いは?
- 不変: 読み取り専用、複数同時OK - 可変: 変更可能、1つだけ、排他的

  • NLLとは?
- Non-Lexical Lifetimes - 参照のライフタイムが最後の使用まで

  • 借用チェッカーは何を防ぐ?
- データ競合 - ダングリング参照 - Use After Free

レベル2: 応用

  • このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);
let r3 = &mut s;  // ?
  • 答え: Yes(NLLにより、r1, r2は終了済み)
  • このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
  • 答え: No(不変借用と可変借用が共存)
  • このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
  • 答え: No(複数の可変借用)
  • レベル3: 設計

  • 関数シグネチャを設計せよ
- 文字列を読むだけ: fn read(s: &str) -> usize - 文字列を変更: fn modify(s: &mut String) - 文字列の一部を返す: fn extract<'a>(s: &'a str) -> &'a str

  • ライフタイムを設計せよ
struct Parser<'a> {
    source: &'a str,
}

impl<'a> Parser<'a> {
    fn parse(&self) -> Result<&'a str, Error> {
        // sourceから一部を抽出して返す
    }
}

次のステップ

Day 3では、ライフタイムについてさらに詳しく学びます:

学習ポイント:

  • ライフタイム注釈の文法
  • 構造体とライフタイム
  • ライフタイムの推論ルール(elision rules)
  • 'static ライフタイム
  • 高度なライフタイムパターン

借用とライフタイムを完全に理解することで、Rustの安全性と表現力を最大限に活用できるようになります。これらはRustの最も重要な概念であり、マスターすることで他の言語では不可能な安全なコードが書けるようになります。