Day 3: ライフタイム理解 - 解説

目次

ライフタイムの本質

ライフタイムとは何か

ライフタイムは、参照が有効である期間をコンパイラに伝える注釈です。重要なのは、これは実行時には存在しない、完全にコンパイル時の概念だということです。

コンパイル時 vs 実行時

// コンパイル時: ライフタイム情報を使って安全性を検証
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// 実行時: ライフタイム情報は完全に消え、単なるポインタ操作
// 機械語レベルでは通常のC言語のポインタと変わらない
// しかし、コンパイル時の検証により安全性が保証されている

アナロジー: ライフタイムは、建物の「設計図に書かれた耐荷重表示」のようなものです:

  • 設計時には必須の情報(コンパイル時)
  • 完成した建物には記載されていない(実行時)
  • でも、その情報に基づいて建物は安全に建てられている

なぜライフタイムが必要か

Rustは「ダングリング参照(無効な参照)」を完全に防ぎたいのです。

C言語の問題

// C言語: ダングリングポインタ(危険!)
char* dangling() {
    char buffer[100];
    return buffer;  // スタック上のローカル変数を返す
}

int main() {
    char* ptr = dangling();
    printf("%s", ptr);  // 未定義動作!クラッシュやセキュリティ脆弱性
}

Rustの解決策

// Rust: コンパイル時にエラー
fn dangling() -> &str {
    let buffer = String::from("temp");
    &buffer  // コンパイルエラー!
}
// error[E0106]: missing lifetime specifier
// error[E0515]: cannot return reference to local variable `buffer`

Rustの保証: > 「すべての参照は、参照先が有効である間のみ生存する」

この保証を実現するのがライフタイムシステムです。

ライフタイムの3つのルール

Rustは多くの場合、ライフタイムを自動推論します。以下の3つのルールが適用されます:

ルール1: 各入力参照に別々のライフタイム

// コンパイラが見るコード
fn print(x: &str, y: &str) {
    println!("{} {}", x, y);
}

// コンパイラが推論した内容
fn print<'a, 'b>(x: &'a str, y: &'b str) {
    println!("{} {}", x, y);
}

理由: 各参照は独立した生存期間を持つ可能性がある

ルール2: 入力が1つなら出力も同じライフタイム

// コンパイラが見るコード
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap()
}

// コンパイラが推論した内容
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap()
}

理由: 返り値は入力から借用したものなので、同じライフタイム

ルール3: selfがあれば出力はselfのライフタイム

struct Parser<'a> {
    data: &'a str,
}

impl<'a> Parser<'a> {
    // コンパイラが見るコード
    fn get_data(&self) -> &str {
        self.data
    }

    // コンパイラが推論した内容
    fn get_data(&self) -> &'a str {
        self.data
    }
}

理由: メソッドの返り値は通常selfから借用する

設計原則

RAII (Resource Acquisition Is Initialization)

Rustのライフタイムは、C++のRAII原則を型システムレベルで強制します。

// RAIIの例: ファイルハンドルの自動クローズ
use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> std::io::Result<String> {
    let mut file = File::open(path)?;  // ファイルを開く(リソース取得)
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
    // ここでfileのライフタイムが終了
    // Dropトレイトが自動実行され、ファイルがクローズされる
}

メモリレイアウト(スタック):

read_file のスタックフレーム
┌─────────────────────┐
│ file: File          │ <- ファイルハンドル
│ contents: String    │ <- バッファ
└─────────────────────┘
       ↓ 関数終了
┌─────────────────────┐
│ (自動的にクリーンアップ)│
│ - File::drop()      │
│ - String::drop()    │
└─────────────────────┘

Borrow Checker(借用チェッカー)

借用チェッカーは、以下のルールを強制します:

  • 排他的アクセス: 可変参照は1つだけ
  • 共有アクセス: 不変参照は複数OK
  • 同時には不可: 可変参照と不変参照は同時に存在できない

// ルール違反の例
fn broken_example() {
    let mut data = String::from("hello");

    let r1 = &data;        // OK: 不変参照
    let r2 = &data;        // OK: 不変参照は複数OK
    let r3 = &mut data;    // エラー!不変参照が存在する間は可変参照は作れない

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

// 正しい例
fn correct_example() {
    let mut data = String::from("hello");

    {
        let r1 = &data;
        let r2 = &data;
        println!("{} {}", r1, r2);
    } // r1, r2 のライフタイムが終了

    let r3 = &mut data;  // OK: 不変参照はもう存在しない
    r3.push_str(" world");
    println!("{}", r3);
}

Non-Lexical Lifetimes (NLL)

Rust 2018以降、ライフタイムはより賢くなりました。

// Rust 2015: エラー
fn old_rust() {
    let mut data = String::from("hello");
    let r = &data;
    println!("{}", r);
    data.push_str(" world");  // エラー!(2015版)
}

// Rust 2018+: OK
fn new_rust() {
    let mut data = String::from("hello");
    let r = &data;
    println!("{}", r);
    // ここでrは最後に使われたので、ライフタイム終了
    data.push_str(" world");  // OK!(2018+版)
}

NLLのメリット:

  • より直感的なコード
  • 不要な制約の削減
  • 保守性の向上

メンタルモデル

ライフタイムをスコープとして理解する

ライフタイムは「時間の範囲」として考えると理解しやすいです。

fn example() {
    // 'outer ライフタイムの開始
    let outer = String::from("outer");

    {
        // 'inner ライフタイムの開始
        let inner = String::from("inner");

        // outerとinnerの両方が有効
        println!("{} {}", outer, inner);

        // 'inner ライフタイムの終了
    }

    // outerのみ有効
    println!("{}", outer);

    // 'outer ライフタイムの終了
}

タイムライン:

時間 →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
outer  ████████████████████████
inner      ████████
           ↑   ↑
           開始 終了

参照のグラフ構造

ライフタイムは、データの依存関係グラフとして理解できます。

struct Node<'a> {
    value: i32,
    parent: Option<&'a Node<'a>>,
}

fn create_tree() -> Node<'static> {
    // 'static な参照を持つツリー
    static ROOT: Node = Node {
        value: 0,
        parent: None,
    };

    Node {
        value: 1,
        parent: Some(&ROOT),
    }
}

依存グラフ:

┌────────────┐
│ ROOT       │
│ (value: 0) │
└────────────┘
      ↑
      │ parent reference
      │
┌────────────┐
│ child      │
│ (value: 1) │
└────────────┘

制約: > childはROOTより長く生存できない(ROOTへの参照を持つため)

ビジュアル図解

メモリレイアウト: スタック vs ヒープ

fn memory_layout() {
    let x = 42;                    // スタック
    let s = String::from("hello"); // ヒープにデータ、スタックに管理情報
    let r = &s;                    // スタックに参照
}

メモリ図解:

スタック(Stack)                  ヒープ(Heap)
┌──────────────────┐              ┌──────────────────┐
│ x: i32           │              │                  │
│ ┌──────┐         │              │                  │
│ │  42  │         │              │                  │
│ └──────┘         │              │                  │
│                  │              │                  │
│ s: String        │              │ "hello"          │
│ ┌──────┐         │      ┌──────>┌──────┬──────┬──────┬──────┬──────┐
│ │ ptr  ├─────────┼──────┘       │ 'h'  │ 'e'  │ 'l'  │ 'l'  │ 'o'  │
│ ├──────┤         │              └──────┴──────┴──────┴──────┴──────┘
│ │ len  │ = 5     │              │                  │
│ ├──────┤         │              │                  │
│ │ cap  │ = 5     │              │                  │
│ └──────┘         │              │                  │
│                  │              │                  │
│ r: &String       │              │                  │
│ ┌──────┐         │              │                  │
│ │ ptr  ├─────┐   │              │                  │
│ └──────┘     │   │              │                  │
└──────────────┼───┘              └──────────────────┘
               │
               └──> s への参照(スタック上のアドレス)

ライフタイムの伝播

fn propagation<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn usage() {
    let s1 = String::from("short");
    let result;

    {
        let s2 = String::from("longer string");
        result = propagation(&s1, &s2);
        // result のライフタイムは s1 と s2 の最短期間
    } // s2 が破棄される

    // ここでresultを使うとコンパイルエラー
    // println!("{}", result); // エラー!
}

ライフタイムダイアグラム:

時間 →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
s1     ████████████████████████████
s2         ████████████
           ↓
'a         ████████████  (s1とs2の共通部分)
           ↓
result     ████████████  ('a と同じ)

使用可能 ────────┘       └──── 使用不可

構造体のライフタイム

struct Excerpt<'a> {
    text: &'a str,
}

fn example() {
    let novel = String::from("Call me Ishmael...");
    let excerpt = Excerpt { text: &novel };

    println!("{}", excerpt.text);
} // novel と excerpt が同時に破棄される

メモリとライフタイムの関係:

┌─────────────────────────────────────┐
│ スタック                             │
│                                     │
│ novel: String                       │
│ ┌────────┐                          │
│ │ ptr    ├───┐                      │
│ ├────────┤   │                      │
│ │ len=19 │   │                      │
│ ├────────┤   │                      │
│ │ cap=19 │   │                      │
│ └────────┘   │                      │
│              │                      │
│ excerpt: Excerpt<'a>                │
│ ┌────────┐   │                      │
│ │ text   ├───┼──┐ ライフタイム 'a   │
│ └────────┘   │  │ は novel に依存   │
└──────────────┼──┼──────────────────┘
               │  │
               ↓  ↓
    ┌──────────────────────────┐
    │ ヒープ                    │
    │ "Call me Ishmael..."     │
    └──────────────────────────┘

制約: excerpt は novel より長く生存できない

重要なポイント

1. ライフタイムは実行時に存在しない

// これらは実行時には同じコード
fn with_lifetime<'a>(s: &'a str) -> &'a str { s }
fn without_lifetime(s: &str) -> &str { s }

// コンパイル後のアセンブリは同一
// ゼロコスト抽象化の典型例

パフォーマンス影響:

  • CPU命令数: 同じ
  • メモリ使用量: 同じ
  • 実行速度: 同じ

2. 最短ライフタイムに合わせる

fn example<'a>(x: &'a str, y: &'a str) -> &'a str {
    // 返り値のライフタイムは x と y のうち短い方
    if x.len() > y.len() { x } else { y }
}

ルール: > 複数の参照が同じライフタイム 'a を持つ場合、'a は最も短い生存期間に制約される

3. 省略可能な場合が多い

統計によれば、87%のケースでライフタイム注釈は省略可能です。

// 省略可能(87%のケース)
impl Parser {
    fn parse(&self, input: &str) -> &str {
        // ライフタイム推論が自動で動作
        &input[0..10]
    }
}

// 明示的に必要(13%のケース)
fn complex<'a, 'b>(
    x: &'a str,
    y: &'b str,
    config: &'a Config,
) -> &'a str {
    // 複雑な関係は明示的に
    x
}

4. 'static は特別

// 文字列リテラルは 'static
let s: &'static str = "hello";

// グローバル変数も 'static
static GLOBAL: i32 = 42;

// 'static の意味: プログラム全体の期間有効

注意: 'static は「永遠に生存」を意味するため、慎重に使うべきです。

// 悪い例: メモリリーク
fn leak() -> &'static str {
    let s = String::from("leaked");
    Box::leak(s.into_boxed_str())  // 意図的にリーク!
}

// 良い例: 実際に静的なデータ
const GREETING: &'static str = "Hello";

セルフチェック質問

以下の質問に答えて、理解度を確認してください。

質問1: 基本理解

Q: 次のコードはコンパイルされますか?なぜですか?

fn foo() -> &str {
    let s = String::from("hello");
    &s
}

答えを見る

A: いいえ、コンパイルされません。

理由:

  • sはローカル変数で、関数終了時に破棄される
  • &ssへの参照なので、sより長く生存できない
  • 関数から返すと、参照先がない「ダングリング参照」になる

エラーメッセージ:

error[E0515]: cannot return reference to local variable `s`

修正方法:

// 修正1: 所有権を返す
fn foo() -> String {
    String::from("hello")
}

// 修正2: 'staticを使う
fn foo() -> &'static str {
    "hello"  // 文字列リテラル
}

質問2: ライフタイム注釈

Q: 次の2つの関数の違いは何ですか?

fn a<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x }
fn b<'a>(x: &'a str, y: &'a str) -> &'a str { x }

答えを見る

A: 関数aの方が柔軟です。

理由:

  • a: xとyが独立したライフタイムを持つ
- yが先に破棄されても、返り値(xの参照)は有効
  • b: xとyが同じライフタイムに制約される
- どちらか一方が破棄されると、返り値も使えなくなる

実用例:

fn usage() {
    let x = String::from("long");
    let result;

    {
        let y = String::from("short");
        result = a(&x, &y);  // OK
        // result = b(&x, &y);  // 関数b の場合、ここでresultも無効化
    }

    println!("{}", result);  // OK (関数a の場合)
}

質問3: 構造体のライフタイム

Q: 次のコードでParserのライフタイム'aは何を表していますか?

struct Parser<'a> {
    input: &'a str,
}

答えを見る

A: Parserインスタンスが参照する文字列データが有効である期間。

意味: > 「この Parser は、input が有効である間しか生存できない」

制約:

// OK
fn ok() {
    let data = String::from("input");
    let parser = Parser { input: &data };
    // parser と data は同時に破棄される
}

// NG
fn ng() -> Parser {
    let data = String::from("input");
    Parser { input: &data }  // エラー!data が先に破棄される
}

質問4: ライフタイム省略

Q: 次のうち、ライフタイム注釈が省略可能なのはどれですか?

// A
fn foo(s: &str) -> &str { s }

// B
fn bar(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// C
impl<'a> Parser<'a> {
    fn parse(&self) -> &str { self.input }
}

答えを見る

A:

  • A: 省略可能 (ルール2: 入力1つ→出力も同じライフタイム)
  • B: 省略不可 (複数入力からの返り値なので、明示的に注釈が必要)
  • C: 省略可能 (ルール3: selfがあれば出力はselfのライフタイム)

正しい完全版:

// A
fn foo<'a>(s: &'a str) -> &'a str { s }

// B
fn bar<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// C
impl<'a> Parser<'a> {
    fn parse(&self) -> &'a str { self.input }
}

質問5: 高度な理解

Q: 次のコードはなぜコンパイルされますか(Rust 2018+)?

fn example() {
    let mut data = vec![1, 2, 3];
    let r = &data[0];
    println!("{}", r);
    data.push(4);  // なぜOK?
}

答えを見る

A: Non-Lexical Lifetimes (NLL) のおかげです。

説明:

  • Rust 2015: rのライフタイムはスコープ全体→ data.push(4)でエラー
  • Rust 2018+: rのライフタイムは最後の使用まで→ println!の後に終了

ライフタイム図:

Rust 2015:
data      ████████████████
r         ████████████████ (スコープ全体)
          ↑           ↑
          使用        push (エラー!)

Rust 2018+:
data      ████████████████
r         ████
          ↑  ↑
          使用 終了
                      ↑
                      push (OK!)

メリット:

  • より直感的なコード
  • 不要な制約の削減

まとめ

ライフタイムの解説を通じて、以下の概念を学びました:

  • 本質: コンパイル時の参照有効期間の注釈
  • 設計原則: RAII、借用チェッカー、NLL
  • メンタルモデル: スコープ、グラフ構造、タイムライン
  • ビジュアル: メモリレイアウト、ライフタイム伝播
  • 重要ポイント: ゼロコスト、省略規則、'static

これらの理解を基に、実践的な課題に取り組み、さらにDay 4のスマートポインタへと進んでください。ライフタイムとスマートポインタを組み合わせることで、Rustの真の力を引き出すことができます。