blog.dopana

Back

ガベージコレクタ(GC)なしでメモリ安全性を保証することは、Rustの最大の強みです。これを実現するために、コンパイラはボローチェッカー(借用検査器)と呼ばれるツールを使用し、すべての参照(Reference)が常に有効なメモリを指していることを確認します。

ボローチェッカーがダングリング参照(すでに破棄されたデータを指す無効な参照)を防ぐために使用する主な仕組みが、ライフタイム(Lifetimes)です。

この記事では、Rustのライフタイムの概念、コンパイラがライフタイムを必要とする理由、ライフタイム注釈の書き方、そしてそのベースとなる考え方について解説します。

問題点:ダングリング参照#

ダングリング参照は、すでに解放されたメモリ上の場所をプログラムが参照したときに発生します。以下は、Rustがコンパイルを拒否するシンプルな例です。

fn main() {
    let r;

    {
        let x = 5;
        r = &x; // ❌ xはこのブロックの終わりで破棄(drop)されます
    }

    println!("r: {r}"); // ❌ rは解放済みのメモリを指しています!
}
rust

このコードをコンパイルしようとすると、Rustは x does not live long enough(xの生存期間が十分に長くありません)というエラーを発生させます。コンパイラは変数のスコープを比較し、参照が所有者よりも長生きしないことを確認します。

gantt
    title xとrのスコープ生存期間
    dateFormat  X
    axisFormat %s
    section 変数 x
    xのスコープ : active, 0, 2
    section 参照 r
    rのスコープ : active, 0, 4

ここで、r0 から 4 まで生存しますが、x0 から 2 までしか生存しません。rx よりも長生きするため、この参照は無効になります。

ライフタイムとは何か?#

ライフタイムとは、参照が有効であるスコープ(生存期間)のことです。ほとんどの場合、ライフタイムは暗黙的であり、コンパイラによって推論されます。しかし、関数や構造体において参照同士の関係性が曖昧な場合、手動でライフタイムを注釈する必要があります。

[!IMPORTANT] ライフタイム注釈は、値の生存期間を変更するものではありません。代わりに、複数の参照のライフタイム間の関係性を記述し、無効なメモリへのアクセスが起こらないことをコンパイラに証明するためのものです。

関数におけるジェネリックライフタイム#

2つの文字列スライスのうち、長い方を返す関数を見てみましょう:

// ❌ コンパイルできません
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
rust

コンパイラは、返される参照が xy のどちらを指しているか判断できないため、このコードを拒否します。もし xy のライフタイムが異なる場合、返された参照がどちらか一方の寿命を超えて生存し、ダングリングポインタを引き起こす可能性があるからです。

これを解決するために、ジェネリックライフタイムパラメータを導入します:

// ✅ コンパイルできます!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
rust
  • <'a> は、'a という名前のジェネリックライフタイムパラメータを宣言します。
  • x: &'a stry: &'a str は、両方の入力参照が少なくとも 'a と同じ長さだけ生存しなければならないことを指定します。
  • -> &'a str は、返される参照もまた少なくとも 'a と同じ長さだけ生存することを保証します。

実際には、戻り値のライフタイム 'a は、入力された xy のライフタイムのうち短い方と等しくなります。

分かりやすい例え(ELI5):アパートのまた貸し#

ライフタイムはアパートの賃貸契約に似ています。

  • 大家(所有者):実際のデータを所有する変数。
  • 店借人(参照):データを使用する借用者。
  • また貸し契約(ライフタイム):借用契約。
大家の契約期間(データ存在):  ====================
また貸し期間(借用中):       ==========      (有効) ✅
また貸し期間(借用中):       ====================== (無効!) ❌
text

あなたのまた貸し契約(参照ライフタイム)は、大家との元の賃貸契約(所有者スコープ)より長生きすることはできません。もし大家の契約が終わった後に部屋に居座り続ければ、無効なメモリへ不法侵入していることになります!

参照を持つ構造体#

構造体が所有権を持つ型ではなく参照を保持する場合、構造体の定義にライフタイム注釈を宣言する必要があります。これにより、構造体のインスタンスが、保持している参照よりも長生きしないことが保証されます。

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

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("'.' が見つかりませんでした");
    
    // 構造体のインスタンスは first_sentence のライフタイムに関連付けられます
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
rust

ここで、ImportantExcerpt のインスタンスは、その part フィールドに格納された参照の寿命を超えて生存することはできません。

ライフタイム省略ルール(省略の3規則)#

これまでに、ライフタイム注釈なしで参照を返す関数を何度も書いたことに気づいたかもしれません。例えば:

fn first_word(s: &str) -> &str { ... }
rust

なぜこのコードは 'a なしでコンパイルできたのでしょうか?

Rust開発チームは、プログラマが同じライフタイムのパターンを繰り返し書いていることに気づきました。開発効率を向上させるため、コンパイラに特定の決定論的ルール(ライフタイム省略規則)を組み込みました:

  1. 参照である各パラメータ(入力)は、それぞれ独自のライフタイムパラメータを取得します(例:fn foo<'a, 'b>(x: &'a i32, y: &'b i32))。
  2. 入力ライフタイムパラメータが正確に1つだけの場合、そのライフタイムがすべての出力参照に割り当てられます(例:fn foo<'a>(x: &'a i32) -> &'a i32)。
  3. 複数の入力ライフタイムパラメータがあり、そのうちの1つが &self または &mut self(メソッドであること)である場合、self のライフタイムがすべての出力参照に割り当てられます。

コンパイラがこれらの規則を適用しても戻り値のライフタイムを解決できない場合にのみ、コンパイルエラーが発生し、手動での注釈が求められます。

スタティックライフタイム#

Rustには 'static という特別なライフタイムがあります。これは、プログラムの実行期間中ずっと参照が有効であることを示します。

文字列リテラルは、データがプログラムのバイナリに直接埋め込まれているため、すべて 'static ライフタイムを持ちます:

let s: &'static str = "私はスタティックライフタイムです。";
rust

[!WARNING] 'static は便利ですが、ボローチェッカーのエラーを一時的に回避するための「安易な解決策」として使用するのは避けてください。本来は一時的であるべき参照を強制的に 'static にすると、設計の破綻やコンパイルエラーにつながります。

まとめ#

  • ライフタイムは、参照がいつまで有効かを表す名前付きの生存期間です。
  • 借用が所有者より長生きしないことを保証し、ダングリング参照を防ぎます。
  • ライフタイム省略規則により、一般的なケースでは注釈の記述を省略できます。
  • 参照を保持する構造体は、メモリ安全性を保証するためにライフタイム境界を宣言する必要があります。

次回の記事では、この知識をベースに、ヒープ上の参照管理をカスタマイズできるスマートポインタ(Smart Pointers)について探求します。

参照#