Day 010 — ライフタイム省略規則 — 明示が不要になる3つのルール

2026-08-07 🔵 中級者 / Phase 2 概念理解 ライフタイム省略規則

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

Day 009では、longest関数がなぜ<'a>を明示しないとコンパイルできないのかを見ました。一方でannounce_and_return_part(&self, announcement: &str) -> &strはライフタイム注釈なしでコンパイルが通りました。「なぜある関数は注釈が要り、別の関数は要らないのか」——この境界線を人間の勘ではなくコンパイラが機械的に判定するための固定ルールが、今日学ぶライフタイム省略規則(lifetime elision rules)です。

省略規則は「推測」ではなく「決定的なアルゴリズム」

重要な前提として、ライフタイム省略は曖昧さを許容する賢い推論ではありません。コンパイラは以下の3つのルールをこの順番で機械的に適用し、すべてのライフタイムを解決できればコンパイルを通し、解決できなければE0106エラーを出して人間に明示を要求します。ルールに当てはまらないケースで「たぶんこっちだろう」と推測することは一切ありません。

📌
ルール1: 各引数の省略されたライフタイムは、それぞれ別々のライフタイムパラメータになる
fn foo(x: &str, y: &str)
// コンパイラ内部ではこう解釈される:
fn foo<'a, 'b>(x: &'a str, y: &'b str)

引数が何個あっても、省略された&にはそれぞれ独立した名前のないライフタイムが割り当てられます。

📌
ルール2: 入力ライフタイム位置がちょうど1つなら、そのライフタイムがすべての省略された出力ライフタイムに割り当てられる
fn first_word(s: &str) -> &str
// コンパイラ内部ではこう解釈される:
fn first_word<'a>(s: &'a str) -> &'a str

引数の参照が1つしかない場合、戻り値の参照が「どの引数由来か」に曖昧さがないため、そのまま流用できます。

📌
ルール3: 複数の入力ライフタイム位置があっても、そのうちの1つが&selfまたは&mut selfなら、selfのライフタイムがすべての省略された出力ライフタイムに割り当てられる
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str
    // コンパイラ内部ではこう解釈される:
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str
}

引数は&selfannouncement: &strの2つですが、「メソッドが返す参照は大抵の場合selfが持っているデータ由来だろう」という設計上の経験則をルールとして固定化し、この場合だけ特別に「自己(self)優先」で解決します。

3つのルールすべてを適用しても解決できない場合

Day 009のlongest(x: &str, y: &str) -> &strはルール1で'a'bという別々のライフタイムが割り当てられますが、入力ライフタイム位置が2つあるためルール2は不適用、&selfもないためルール3も不適用です。結果、戻り値の&strのライフタイムが未解決のまま残り、コンパイラはE0106: missing lifetime specifierを出して人間に明示を要求します。

📝 問題

  1. 概念理解課題: 3つのライフタイム省略ルールを自分の言葉で説明してください。また、なぜこれらが「曖昧な状況での推測」ではなく「決定的なアルゴリズム」だと言えるのか、理由を添えて説明してください
  2. 判定課題: 以下の4つの関数・メソッドシグネチャそれぞれについて、(a) ライフタイム注釈なしでコンパイルが通るか、(b) 通るならどの規則(ルール1〜3)が適用されて解決するか、(c) 通らないなら理由、を答えてください
    fn print_str(s: &str) {}
    
    fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or("")
    }
    
    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() { x } else { y }
    }
    
    struct Parser;
    impl Parser {
        fn peek<'b>(&self, s: &'b str) -> &str {
            s
        }
    }
  3. デバッグ予測課題: 以下のcombine関数はコンパイルエラーになります。何というエラーコードが出るか、そして3つの省略ルールをそれぞれ順番に適用してみて「なぜどのルールも解決に使えないのか」を説明してください
    fn combine(a: &str, b: &str, c: &str) -> &str {
        if a.len() >= b.len() && a.len() >= c.len() { a }
        else if b.len() >= c.len() { b }
        else { c }
    }
  4. 実装課題: Day 009で作ったImportantExcerpt<'a>構造体(フィールドpart: &'a str)に、ライフタイム注釈を一切書かずに(省略ルール3に任せて)以下の2つのメソッドを追加してください
    • fn part_len(&self) -> usizeself.partの文字数(バイト数)を返す
    • fn first_word(&self) -> &strself.partの最初の単語(空白区切り)を返す
    両方ともコンパイルが通ることを、テストで確認してください

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問題2は、各シグネチャの「引数のうち&が付いているものの数」をまず数え、それが0個・1個・2個以上のどれかで場合分けすると規則が機械的に決まります
  • 問題3は、combineの引数abcにルール1で別々のライフタイムを割り振ったあと、ルール2(入力ライフタイム位置が1つ)とルール3(&selfの有無)がそれぞれ本当に条件を満たすか、1つずつ確認してください
  • 問題4はfn first_word(&self) -> &strが、Day 009のannounce_and_return_part(&self, announcement: &str) -> &strと同じ形(&selfがあればルール3が働く)であることに気づけるかがポイントです
ヒント2 — アプローチ
  • 問題2のParser::peek&selfs: &'b strという2つの入力ライフタイム位置がありますが、戻り値の&strにはすでに'bという明示のライフタイムが付いていません(省略されています)。ここでルール3が「&selfがあるので戻り値はself由来」と機械的に決定します。つまり戻り値は'b(=引数sのライフタイム)ではなく、selfのライフタイムに紐づくと解釈される点に注意してください
  • 問題3のエラーコードはE0106です。ルール1適用後、abcにはそれぞれ独立したライフタイムが割り振られるため「入力ライフタイム位置が1つ」というルール2の前提が崩れます。&selfも存在しないためルール3も適用対象外です
  • 問題4はfn part_len(&self) -> usizeのように戻り値に参照を含まない場合、そもそもライフタイム省略規則が関与する余地がない(usizeは借用ではない)ことにも触れられると理解が深まります
ヒント3 — コード骨格
struct ImportantExcerpt<'a> {
    part: &'a str,
}

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

    // 追加するメソッドはここに書く。ライフタイム注釈は不要(省略規則が解決する)
    fn part_len(&self) -> usize {
        self.part.len()
    }

    fn first_word(&self) -> &str {
        self.part.split_whitespace().next().unwrap_or("")
    }
}

模範解答

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

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

    // 省略ルール3: &self があるので、戻り値がもし参照を含んでいれば self のライフタイムに
    // 紐づけられる。ただしこのメソッドの戻り値は usize(借用ではない)なので、
    // そもそもライフタイム解決の対象にすらならない。
    fn part_len(&self) -> usize {
        self.part.len()
    }

    // 省略ルール3が実際に働くケース。引数は &self のみ(ルール1で 'a が割り当て済み)。
    // 入力ライフタイム位置は1つなのでルール2でも解決できるし、&self があるのでルール3でも
    // 同じ結論(self 由来)に至る。コンパイラ内部の解釈は次と等価:
    //   fn first_word<'b>(&'b self) -> &'b str
    fn first_word(&self) -> &str {
        self.part.split_whitespace().next().unwrap_or("")
    }
}

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);

    println!("part_len = {}", excerpt.part_len());
    println!("first_word = {}", excerpt.first_word());
}

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

    #[test]
    fn test_part_len() {
        let excerpt = ImportantExcerpt::new("Call me Ishmael");
        assert_eq!(excerpt.part_len(), 16);
    }

    #[test]
    fn test_first_word() {
        let excerpt = ImportantExcerpt::new("Call me Ishmael");
        assert_eq!(excerpt.first_word(), "Call");
    }

    #[test]
    fn test_first_word_single_word() {
        let excerpt = ImportantExcerpt::new("Ishmael");
        assert_eq!(excerpt.first_word(), "Ishmael");
    }
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
part_len = 16
first_word = Call

$ cargo test
running 3 tests
test tests::test_part_len ... ok
test tests::test_first_word ... ok
test tests::test_first_word_single_word ... ok

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

🪜 Step-by-Step 解説

1
問題2の4シグネチャを規則に当てはめる
fn print_str(s: &str) {}
引数の入力ライフタイム位置は1つ(s)ですが、戻り値がそもそも参照を含まない(戻り値なし)ため、出力ライフタイムを解決する必要自体がありません。省略規則の適用対象外で、単純にコンパイルが通ります。
fn first_word(s: &str) -> &str { ... }
入力ライフタイム位置が1つ(ルール1で'aが割り当て)→ルール2が適用され、戻り値の&str'aに解決されます。コンパイル可能です。
fn longest(x: &str, y: &str) -> &str { ... }
入力ライフタイム位置が2つ(ルール1で'a'b)→ルール2は「ちょうど1つ」の場合しか適用されないため不成立。&selfも存在しないためルール3も不成立。戻り値のライフタイムが未解決のまま残り、E0106でコンパイルエラーになります(Day 009で確認済み)。
impl Parser {
    fn peek<'b>(&self, s: &'b str) -> &str { s }
}
入力ライフタイム位置は2つ(&selfs: &'b str)。ルール2は不成立ですが、&selfがあるためルール3が適用され、戻り値の&strselfのライフタイムに解決されます。ここが引っかかりやすいポイントです——関数の中身はs(引数)を返しているのに、コンパイラが型シグネチャだけを見て機械的に「戻り値はself由来」と決めてしまうため、実際にはsのライフタイムの方が短い場合など、呼び出し元の使い方次第ではコンパイルエラー(借用エラー)になる可能性があります。ルールは関数の中身を見ずにシグネチャだけで解決するため、こうした「意図とルールの適用結果がズレる」ケースが実務でも起こり得ます。
2
問題3のcombineをルール順に検証する
fn combine(a: &str, b: &str, c: &str) -> &str
  • ルール1適用: abcにそれぞれ独立したライフタイム'a'b'cが割り当てられる
  • ルール2判定: 入力ライフタイム位置は3つ(1つではない)→不成立
  • ルール3判定: 引数の中に&self&mut selfが存在しない→不成立
  • 結果: 戻り値&strのライフタイムが未解決のまま残り、コンパイラはE0106: missing lifetime specifierを出す
3
問題4で省略ルール3が実際にコードを簡潔にする様子
fn first_word(&self) -> &str {
    self.part.split_whitespace().next().unwrap_or("")
}
このメソッドを明示的なライフタイム注釈で書くとfn first_word<'b>(&'b self) -> &'b strになりますが、impl<'a> ImportantExcerpt<'a>ブロック内では&selfの型は実質&'a ImportantExcerpt<'a>から借用された&'b self'bは呼び出しごとに変わる短い借用期間)であり、戻り値はself.part(本来のライフタイムは'a)を渡していますが、コンパイラの型検査上は「'b(自己の借用期間)に制約された&str」として扱われます。省略規則によって、この一連の型推論をプログラマが一切書かずに済ませられるわけです。

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

📌
典型パターンの自動補完: 省略規則は「Rustは安全性のために冗長な記述を強制する言語だ」という印象を和らげるための、意図的な人間工学的トレードオフです。Rustの設計チームは大量の実際のRustコードを分析し、関数シグネチャの大多数が3つの典型パターン(単一引数→単一戻り値、&selfメソッド、参照を返さない)のいずれかに収まることを確認しました。そのうえで、「ルールが機械的に解決できる場合のみ省略を許可し、解決できない場合は必ず人間に明示を求める」という設計にすることで、曖昧さのないコンパイル時保証を一切妥協せずに、典型パターンでの冗長さだけを排除しています。
⚠️
ルールの限界: Step 1で見たParser::peekのような「ルールの機械的な適用結果が、人間の直感(sを返しているのだからs由来のはず)とズレる」ケースは、この設計の限界でもあります。Rustはここで「実行時に何が起きるか」ではなく「型シグネチャという静的な情報だけで判断する」ことを徹底しているため、こうしたズレが生じたときは、プログラマが明示的にライフタイムを書いてルールを上書きする必要があります。

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

問題3のcombine(ライフタイム注釈なし、3引数)です。

fn combine(a: &str, b: &str, c: &str) -> &str {
    if a.len() >= b.len() && a.len() >= c.len() { a }
    else if b.len() >= c.len() { b }
    else { c }
}
error[E0106]: missing lifetime specifier --> src/main.rs:1:47 | 1 | fn combine(a: &str, b: &str, c: &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 `a`, `b`, or `c`

読み方:

  • error[E0106]: Day 009と同じエラーコードです。省略規則の3つとも解決に使えなかったことを示しています
  • expected named lifetime parameter: 戻り値の&strにライフタイムパラメータが必要だが、シグネチャに書かれていないという意味です
  • helpメッセージの「borrowed from a, b, or c」の部分が、引数が2つのとき(x, yのみ)より選択肢が1つ増えている点に注目してください——引数が増えても、コンパイラのエラーメッセージの構造自体は同じです

修正方法: 実際に返り得る全ての引数(abcすべて)に同じライフタイムパラメータ'aを付ける必要があります。

fn combine<'a>(a: &'a str, b: &'a str, c: &'a str) -> &'a str {
    if a.len() >= b.len() && a.len() >= c.len() { a }
    else if b.len() >= c.len() { b }
    else { c }
}

🌐 他言語との比較

観点RustGoJavaC++TypeScript
「型推論の一種」としての省略規則ライフタイム省略は3つの固定ルールに基づく決定的アルゴリズム。曖昧なら必ずコンパイルエラーで人間に明示を求める参照概念自体がなくGCが管理するため、該当する仕組みが存在しない該当なし該当なし(テンプレートやauto型推論はあるが、参照の生存期間ルールとは別物)型推論はあるが、生存期間ではなく値の型を推論するもので、目的が異なる
ルールが人間の意図とズレた場合の挙動コンパイルが通っても、想定と異なるライフタイムに解決され、呼び出し側で借用エラーになることがある(Parser::peek参照)該当なし該当なし該当なし該当なし

省略規則に最も近い概念を持つ言語はRust以外にほぼ存在しません。GC言語(Go/Java/TypeScript)では参照の生存期間をランタイムが管理するため、そもそも「ライフタイムを省略できるかどうか」という問いが発生しません。C++のテンプレート引数推論やautoは「型」を推論する仕組みであり、「参照同士の生存期間の関係」を推論する仕組みではないため、目的も適用範囲も異なります。

🏆 実務での使いどころ

  • APIレビューでの「なぜここだけ<'a>が必要なのか」判断: 大規模なコードベースでは、大多数の関数がライフタイム注釈なしで書かれています。レビュー中に明示的な<'a>が付いた関数を見かけたら、それは「3つの省略ルールでは解決できない、意図的な関係の明示」であるサインとして読めるようになります
  • ライブラリ設計時のシグネチャ簡潔化: &selfを含むgetter系メソッドの大半は省略規則(ルール3)のおかげで注釈不要になります。逆に、複数の借用を受け取って1つを返すような関数(combineのような設計)では、意図的にライフタイムを明示しないと呼び出し側が混乱するため、設計段階で「この関数は省略規則で解決できるか」をチェックする習慣が役立ちます
  • clippyやコンパイラの提案の理解: rustcが「lifetime elision」に関する警告やヒントを出す場面(例: needless_lifetimes)で、省略規則の仕組みを理解していれば、提案されたコードが安全かどうかを自分で判断できます

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
省略規則が「関数の中身を読んで」ライフタイムを決めていると思い込むfirst_wordのように中身を見れば戻り値の由来が明らかなケースが多いため省略規則はシグネチャの(引数の数・&selfの有無)だけを見る機械的なルールで、関数の中身(ifの分岐など)は一切考慮しない
&selfがあれば常にルール3で解決すると思い、他のルールとの優先順位を意識しないルール3が最も目立つ・覚えやすいため3つのルールは順番に適用される。ルール2(入力位置が1つ)で解決できるならその時点で終わり、&selfの有無に関わらずルール3は評価されない場面もある(例: first_word(s: &str)&selfがなくてもルール2だけで解決する)
ルールで解決できない=バグだと思うE0106がコンパイルエラーとして出ることに驚く初学者が多いルールで解決できないケースは、longestcombineのように「本質的に複数の参照のうちどれが返るか分岐で決まる」設計であることが多く、これは曖昧さが実在することをコンパイラが正しく検出できている証拠。バグではなく、人間による明示が必要な設計判断のポイント

🚀 次のステップ

  • 発展: Parser::peekのように「省略ルールの機械的な結論」と「関数の中身が実際にやっていること」がズレるケースを自分でもう1つ作り、実際にcargo buildしてどんなエラー(または警告)が出るか確認してみてください
  • 次回予告: Day 011ではトレイト境界とwhere句を扱います。ジェネリクスに制約を課す書き方を学び、「どんな型でも受け付ける」設計から「特定の能力を持つ型だけを受け付ける」設計への発展を扱います

🎯 自己評価

自分の回答

気づき・メモ