Day 009 — ライフタイムの基礎 — 参照の生存期間をコンパイラに伝える

2026-08-06 🔵 中級者 / Phase 2 概念理解→実装 ライフタイムの基礎

📚 背景知識(読んでから問題へ)

Phase 1では「借用(&/&mut)」と、それをコンパイラが実際にどう追跡しているか(NLL)を学びました。今日からPhase 2に入り、その借用の裏側で常に動いているライフタイムという仕組みに正面から向き合います。

ライフタイムは「参照の生存期間を延ばす」ものではない

最初に誤解しやすいポイントを潰しておきます。ライフタイム注釈('aのような記法)は、参照の実際の生存期間を変更したり延長したりする機能ではありません。値がいつ生成されいつドロップされるかは、あくまでコード上のスコープ(変数がいつ{}を抜けるか)によって決まります。ライフタイム注釈がしているのは、「複数の参照の生存期間が互いにどういう関係にあるか」をコンパイラに申告することだけです。

なぜ申告が必要になる場面があるのか。次のコードを見てください。

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

このコードは一見自然に見えますが、コンパイルが通りません。理由は、戻り値の&strが「x由来の参照なのかy由来の参照なのか」が実行時の分岐(ifの結果)によって変わるため、コンパイラが静的に決定できないからです。借用チェッカーは「戻り値の参照が指す先が、呼び出し元でいつまで有効か」を知る必要がありますが、xyのどちらを返すかわからない以上、戻り値の有効期間をx基準にもy基準にも決め打ちできません。

ライフタイム注釈で「関係」を明示する

ここでライフタイム注釈を使い、「xy・戻り値は同じ生存期間の制約を共有する」とコンパイラに伝えます。

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

<'a>は関数のジェネリック型パラメータと同じ位置に書くライフタイムパラメータです。これは「xy、戻り値の参照は、少なくとも'aという共通の期間だけ有効である」という制約を宣言しています。実際に渡される'aの長さは呼び出しごとに変わってよく、コンパイラは呼び出し側で「xyのうち短い方の生存期間」を'aとして採用し、戻り値がその期間を超えて使われていないかをチェックします。

📌
重要なのは、この注釈が新しい制約を作っているのではなく、もともと存在していた関係を言葉にしているだけという点です。コンパイラはlongest関数の中身(ifの分岐)を実行時まで解析できないため、人間が「戻り値はxyのどちらかの生存期間に縛られる」という事実を型シグネチャの形で明示的に伝える必要があります。

構造体に参照を持たせる場合も同様

構造体のフィールドに参照を持たせる場合も、同じ理由でライフタイムパラメータが必須になります。

struct ImportantExcerpt<'a> {
    part: &'a str,
}

これは「ImportantExcerptのインスタンスは、partが指している元の文字列データより長生きしてはいけない」という制約を型に埋め込んでいます。もし元の文字列がドロップされた後もこの構造体が生き残れてしまうと、partはダングリング参照(解放済みメモリを指す参照)になってしまいます。ライフタイムパラメータは、この「構造体 ⊆ 参照先データの生存期間」という関係をコンパイル時に強制する仕組みです。

📝 問題

  1. 概念理解課題: ライフタイム注釈('a)は何をコンパイラに伝えるものですか。「参照の生存期間を延長する」という誤解との違いを明確にして説明してください
  2. デバッグ予測課題: 以下のlongest関数はコンパイルエラーになります。何というエラーコードが出るか、そしてコンパイラが「戻り値の生存期間を決定できない」と判断する理由を、ifの分岐の観点から説明してください
    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() { x } else { y }
    }
  3. 実装課題: 以下の仕様を満たすImportantExcerpt<'a>構造体と、そのannounce_and_return_partメソッドを実装してください。
    • フィールドpart: &'a strを持つ
    • fn announce_and_return_part(&self, announcement: &str) -> &strというメソッドを実装し、呼び出し時にannouncementprintln!で表示したうえで、self.partを返す
    • newという関連関数(コンストラクタ)を用意し、渡された&strからImportantExcerptを作る
    • テストで、Stringから最初の文を切り出してImportantExcerptを作り、announce_and_return_partの戻り値が元の文と一致することを確認する
  4. 発展の設計課題(コードは不要): 関数のAPIを設計するとき、戻り値を「ライフタイム付きの&str(借用)」にするか「所有権を持つString」にするかの判断基準を、呼び出し側の使い勝手とパフォーマンスの両面から考えてください

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問題2は「ifの2つの分岐がそれぞれ別の変数を返している」ことに注目してください。コンパイラは実行前にどちらの分岐を通るか分からないため、戻り値の型シグネチャだけを見て判断する必要があります
  • 問題3は本文のImportantExcerpt<'a>の説明をそのまま実装に落とし込む問題です。メソッドの戻り値&strのライフタイムは、Rustのライフタイム省略規則&selfがあるメソッドは戻り値のライフタイムがselfに紐づくと自動推論される)によって、<'a>を明示的に書かなくても通ります
ヒント2 — アプローチ
  • 問題2のエラーコードはE0106(missing lifetime specifier)です。エラーメッセージには「this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from x or y」という趣旨の説明が出ます
  • 問題3のnew関連関数はfn new(part: &str) -> ImportantExcerpt { ImportantExcerpt { part } }のような形になりますが、構造体定義に<'a>があるため、返り値の型もImportantExcerpt<'_>(または省略)としてライフタイムを引き継ぐ必要があります
  • 「最初の文を切り出す」にはs.split('.').next().unwrap()のようなイテレータ操作が使えます
ヒント3 — コード骨格
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn new(part: &'a str) -> ImportantExcerpt<'a> {
        ImportantExcerpt { part }
    }

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = ImportantExcerpt::new(first_sentence);
    let part = excerpt.announce_and_return_part("重要な一文です");
    println!("part = {}", part);
}

模範解答

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    // コンストラクタ。part は呼び出し元の &str を借用するだけなので、
    // ImportantExcerpt<'a> のインスタンスは part の参照元より長生きできない。
    fn new(part: &'a str) -> ImportantExcerpt<'a> {
        ImportantExcerpt { part }
    }

    // 戻り値の &str にライフタイム注釈がないのは省略規則のおかげ。
    // &self があるため、戻り値のライフタイムは self(= 'a)に紐づくと
    // コンパイラが自動的に推論する。
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

// 問題2のエラーを修正した longest(比較用に同梱)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = ImportantExcerpt::new(first_sentence);
    let part = excerpt.announce_and_return_part("重要な一文です");
    println!("part = {}", part);

    println!("longest = {}", longest("short", "much longer string"));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_important_excerpt() {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let first_sentence = novel.split('.').next().unwrap();
        let excerpt = ImportantExcerpt::new(first_sentence);

        let part = excerpt.announce_and_return_part("テスト用の告知");
        assert_eq!(part, "Call me Ishmael");
    }

    #[test]
    fn test_longest() {
        assert_eq!(longest("short", "much longer string"), "much longer string");
        assert_eq!(longest("equal", "equal"), "equal");
    }
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
Attention please: 重要な一文です
part = Call me Ishmael
longest = much longer string

$ cargo test
running 2 tests
test tests::test_longest ... ok
test tests::test_important_excerpt ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

🪜 Step-by-Step 解説

1
なぜImportantExcerpt<'a><'a>が必要なのか
struct ImportantExcerpt<'a> {
    part: &'a str,
}
構造体がフィールドとして参照型(&strなど、値そのものではなく他の場所にあるデータへの参照)を持つ場合、Rustは必ずライフタイムパラメータを要求します。これは「この構造体のインスタンスが生きている間、参照先のデータも必ず生きていなければならない」という制約を型検査に組み込むためです。<'a>を省略すると、コンパイラは「partが指す先がいつまで有効かわからないまま構造体を使わせてよいのか」を判断できず、missing lifetime specifier(E0106)で弾かれます。
2
new関連関数でライフタイムがどう伝播するか
fn new(part: &'a str) -> ImportantExcerpt<'a> {
    ImportantExcerpt { part }
}
引数part: &'a str'aと、戻り値ImportantExcerpt<'a>'aは同じライフタイムパラメータです。これにより「渡された&strの生存期間が、そのまま返されるImportantExcerptの生存期間の上限になる」という関係が保証されます。呼び出し元でnovel(元のString)がドロップされた後にexcerptを使おうとすると、この制約のおかげでコンパイルエラーになります。
3
メソッドの戻り値でライフタイム注釈が省略できる理由
fn announce_and_return_part(&self, announcement: &str) -> &str {
    println!("Attention please: {}", announcement);
    self.part
}
このメソッドは&selfannouncement: &strという2つの参照を引数に取りますが、戻り値の&strにライフタイム注釈がありません。これはライフタイム省略規則の第3規則&selfまたは&mut selfが引数にある場合、戻り値のライフタイムは自動的にselfのライフタイムと同じとみなされる)が適用されているためです。人間が読むときは「戻り値はself.part'aライフタイム)に紐づく」と分かりますが、コンパイラも同じ推論を機械的に行い、明示を省略可能にしています。
4
longestの修正が示す「関係の明示」
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
xy・戻り値すべてに同じ'aを付けることで、「戻り値はxyのうち短い方の生存期間までしか有効ではない」という制約が生まれます。呼び出し側では、xyのライフタイムのうち短い方(コンパイラ用語で「共通部分(intersection)」)が'aとして採用されます。

💡 設計思想・なぜこう書くのか

📌
安全性の証明を型シグネチャに埋め込む: Rustのライフタイムシステムは、「参照を安全に使うための情報は、実行時ではなく型シグネチャの中に静的に埋め込む」という設計哲学の表れです。GCを持つ言語(Go・Java・TypeScript)では、参照先のデータがいつ解放されるかをランタイムのガベージコレクタが動的に判断するため、プログラマがその生存期間を意識する必要はほとんどありません。一方Rustにはガベージコレクタがなく、コンパイル時にすべての参照が安全であることを証明し切る必要があります。ライフタイムパラメータは、その証明に必要な「参照同士の生存期間の関係」を、関数やデータ構造のインターフェース(シグネチャ)の一部として明文化する仕組みです。
📌
実行時コストゼロ: ライフタイム注釈は完全にコンパイル時の情報であり、コンパイルされたバイナリには一切の痕跡を残しません(型消去されます)。これは「ゼロコスト抽象化」というRustの中心的な設計原則——安全性のための仕組みが実行時性能を犠牲にしない——をライフタイムの領域でも体現しています。また、ライフタイム省略規則(Step 3で見た&selfの例)は、「毎回明示させると冗長になりすぎる典型パターンは自動推論に任せる」という人間工学上のトレードオフです。大多数の関数シグネチャが従う3つの典型パターンを分析し、それらに該当する場合のみ注釈を省略できるようにすることで、複雑な生存期間の関係を持つ少数のケースでは明示を要求しつつ、日常的なコードでは冗長な記述を避けるというバランスを取っています。

🛑 コンパイルエラーが出た場合

問題2のlongest(ライフタイム注釈なし)です。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
error[E0106]: missing lifetime specifier --> src/main.rs:1:33 | 1 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

読み方:

  • error[E0106]: ライフタイム注釈が必要な箇所で省略されていることを示すエラーコードです
  • expected named lifetime parameter: 戻り値の&strがどの引数のライフタイムに紐づくのか、コンパイラが特定できないことを示しています
  • helpメッセージがそのものずばり原因を教えてくれています——「戻り値が借用値を含むが、x由来かy由来か分からない」

修正方法: xy・戻り値すべてに同じライフタイムパラメータ'aを付け、「戻り値はxyの両方の生存期間の制約を受ける」と明示します(模範解答のlongest<'a>を参照)。

🌐 他言語との比較

観点RustGoJavaC++TypeScript
参照が指す先の生存期間の保証コンパイル時にライフタイム注釈で静的に検証。違反はコンパイルエラーガベージコレクタが到達可能性で自動管理。ダングリング参照はそもそも発生しないガベージコレクタが自動管理。同様にダングリング参照は起きないプログラマが手動で管理。生存期間の不整合はダングリングポインタとして実行時に顕在化しうるガベージコレクタが自動管理。ダングリング参照の概念はない
構造体が参照を保持する場合の制約表現ライフタイムパラメータ<'a>で型シグネチャに明示。コンパイラが強制該当なし(GCがあるため参照の保持期間を型で表現する必要がない)該当なしスマートポインタ(shared_ptr等)や規約でカバーするが、コンパイラによる強制はない該当なし
学習コストの所在「参照の関係」を型として書く必要があり、初学者には抽象的に感じられやすい参照管理を意識する必要がなく学習コストが低い同上手動管理特有のバグ(use-after-freeなど)の危険性が学習コストとして残る参照管理を意識する必要がなく学習コストが低い

GC言語では「参照先がいつまで有効か」をランタイムが保証してくれるため、そもそもライフタイムという概念自体が言語仕様に現れません。C++は逆に、生存期間の管理をほぼ全面的にプログラマの規律に委ねており、ImportantExcerptのようなクラスを書いても、参照元のstd::stringが先に破棄されてしまうミスをコンパイラは検出できません。Rustのライフタイムは、この「C++的な手動管理の危険性」と「GC言語的な実行時オーバーヘッド」の両方を避けつつ、コンパイル時に安全性を証明するための独自の中間解です。

🏆 実務での使いどころ

  • パーサー・字句解析器の実装: 入力文字列全体を1回だけ確保し、トークンを&strスライスとして切り出して返す設計は非常に一般的です。この際、各トークンを表す構造体(例: Token<'a> { text: &'a str, .. })にライフタイムパラメータを付けることで、元の入力バッファより長生きするトークンが作られてしまう事故をコンパイル時に防げます
  • 設定ファイルやログの一時的な参照ビュー: 大きな文字列やバイト列(例: 読み込んだファイル全体)から、特定の範囲だけを指す「ビュー」構造体を作る場面で、ImportantExcerptと同じパターンが使われます。コピーを避けてメモリ効率を上げつつ、参照元が生きている間しか使えないという安全性も同時に得られます
  • キャッシュや借用ベースのAPI設計: 呼び出し元が所有するデータへの参照を返すAPI(&selfから&str&Tを返すgetterなど)は、Step 3で見たライフタイム省略規則の恩恵を日常的に受けています。プロダクションコードのgetterメソッドのほとんどはこの省略規則のおかげで注釈なしで書けています

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
'aが参照の生存期間を「延長」する魔法だと思い込む「ライフタイムを指定したらエラーが消えた」という体験から、注釈が何かを変えていると錯覚しやすい'aは既存の生存期間の関係を申告しているだけで、実際のデータがいつドロップされるかは変数のスコープのみで決まる。注釈自体は何もデータの寿命を変えない
すべての関数にライフタイム注釈が必要だと思い込むlongestのような複数参照が絡む例が印象に残りやすい単一の参照を引数に取り単一の参照を返す関数や、&selfを含むメソッドの多くは、省略規則により注釈不要でコンパイルが通る
ライフタイムパラメータをジェネリック型パラメータ(<T>)と混同するどちらも<>内に書く記法で見た目が似ているライフタイムパラメータ('a)は「参照同士の生存期間の関係」を表し、型パラメータ(T)は「どの型が入るか」を表す。役割が全く異なる別の概念

🚀 次のステップ

  • 発展: longest関数を、xyが異なるライフタイムを持っていても呼び出せるように、戻り値のライフタイムだけを短い方に合わせる書き方('a: 'bのようなライフタイム境界)を調べてみてください
  • 次回予告: Day 010ではライフタイム省略規則を体系的に扱います。今日Step 3で触れた「なぜ&selfのメソッドは注釈不要なのか」を、3つの省略ルールとして正式に整理します

🎯 自己評価

自分の回答

気づき・メモ