あっぽログ
← 記事一覧に戻る

Rustのライフタイムをコードとともにやさしくひも解く

ライフタイムとは何か?

Rustを学んでいると、こんなエラーに出会うことがあります。

error[E0106]: missing lifetime specifier

「ライフタイム(lifetime)」はRustの中でも特に難しいと言われる概念のひとつです。しかし、ライフタイムが存在する理由を理解すると、その必要性がよくわかります。

ライフタイムとは、一言で言うと参照が有効な期間のことです。RustはGCを持たない代わりに、コンパイル時に「この参照はいつまで使えるか」をチェックすることで、ダングリングポインタ(無効なメモリを指す参照)を防ぎます。


ライフタイムがなぜ必要か:ダングリング参照の例

まず、ライフタイムが必要になる状況を見てみましょう。

fn main() {
    let r;
    {
        let x = 5;
        r = &x; // xへの参照をrに代入
    } // ここでxはドロップされる
    println!("{}", r); // rはもう無効な参照!
}

このコードはコンパイルエラーになります。x はブロックを抜けた時点で破棄されますが、r はそのアドレスを参照したままです。Rustのコンパイラはこれをコンパイル時に検出して防いでくれます。


関数でライフタイム注釈が必要になる場面

ライフタイム注釈('a などの記法)が必要になる典型的なケースは、複数の参照を受け取って、参照を返す関数です。

たとえば、2つの文字列スライスを受け取り、長い方を返す関数を考えてみましょう。

// これはコンパイルエラーになる
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

エラーの理由は、コンパイラが「返す参照が xy のどちらの寿命に縛られるのか」を判断できないからです。

ライフタイム注釈を追加する

ライフタイム注釈 'a を使って、「返す参照の有効期間は xy の短い方に合わせる」とコンパイラに教えます。

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

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("最長の文字列: {}", result); // ここではOK
    }
}

'a具体的な期間を決めるものではなく、「引数と戻り値の参照の寿命に関係性がある」ことをコンパイラに伝えるための**アノテーション(注釈)**です。


構造体にライフタイムを使う

構造体が参照を保持する場合も、ライフタイム注釈が必要です。

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

fn main() {
    let novel = String::from("昔々あるところに。第二章...");
    let first_sentence = novel.split('。').next().expect("文章が見つかりません");

    let excerpt = Excerpt {
        content: first_sentence,
    };

    println!("抜粋: {}", excerpt.content);
}

Excerpt 構造体は novel の参照を保持しているため、novel が生きている限り excerpt も有効です。コンパイラはこの関係を 'a によって追跡します。


ライフタイム省略規則(Elision Rules)

「でも、借用を返す関数を書いたとき、毎回注釈を書いた記憶がない…」と思った方は正しいです。Rustにはライフタイム省略規則があり、多くの場合は自動的に推論されます。

// 注釈なしで書ける例
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

この関数では引数が1つだけなので、戻り値の参照はその引数と同じライフタイムを持つと自動推論されます。省略規則が適用できない複雑なケースでのみ、明示的な注釈が必要になります。


まとめ

概念ポイント
ライフタイムとは参照が有効な期間のこと
なぜ必要かダングリング参照をコンパイル時に防ぐため
注釈の書き方'a などの記号で参照間の関係を示す
省略規則多くの場合はコンパイラが自動推論してくれる

ライフタイムは最初は戸惑いますが、「コンパイラが参照の有効期間を追跡するためのヒント」だと考えると自然に理解できるようになります。エラーが出たら「どの参照が、いつまで生きているか」を丁寧に追ってみましょう。

← 記事一覧に戻る