[Rustのはじめ方] Part24: ライフタイム注釈の実例

Rust

Rustの大きな特徴である「所有権」システムには、「ライフタイム」という概念が密接に関わっています。ライフタイムは、参照が有効であるスコープ(範囲)を示すものです。多くの場合、Rustコンパイラはライフタイムを自動的に推論してくれます(ライフタイム省略)。しかし、コンパイラだけでは判断できない曖昧な状況も存在します。

特に、関数の引数や戻り値、構造体のフィールドで参照を使う場合、コンパイラは参照がどれくらいの期間有効なのかを知る必要があります。もし無効な参照(ダングリングポインタ)を許してしまうと、メモリ安全性が損なわれてしまうからです 💣。

このようなコンパイラが判断できない状況で、私たちが参照の有効期間の関係性を明示的に伝えるのがライフタイム注釈です。今回は、具体的なコード例を見ながらライフタイム注釈の使い方を学んでいきましょう!

ライフタイム注釈は、アポストロフィ(')に続けて小文字の識別子(通常は 'a, 'b など)で表されます。これはジェネリック型パラメータに似ていますが、型ではなくライフタイム(スコープ)を表します。

例:

  • &'a i32: ライフタイム 'a を持つ i32 への参照
  • &'a mut i32: ライフタイム 'a を持つ i32 への可変参照

ライフタイム注釈自体がライフタイムの長さを変えるわけではありません。あくまで、複数の参照がある場合に、それらのライフタイムがどのように関連しているかをコンパイラに伝えるためのものです。

複数の参照を引数に取り、そのうちの1つの参照を返す関数を考えてみましょう。以下の例は、2つの文字列スライスを受け取り、長い方を返す関数です。

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

このコードはコンパイルエラーになります。なぜなら、コンパイラは戻り値の参照が xy のどちらのライフタイムに従うべきか判断できないからです。戻り値の参照は、xy両方が有効な期間だけ有効でなければなりません。

ここでライフタイム注釈を使います。共通のライフタイム 'a を導入し、引数 x, y と戻り値がすべて同じライフタイムを持つことを示します。

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

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz"; // 文字列リテラル

    let result;
    {
        let string3 = String::from("efghijkl");
        result = longest(string1.as_str(), string3.as_str()); // string1 と string3 のうち短い方のライフタイムに束縛される
        println!("The longest string is: {}", result); // ここでは string3 のライフタイム内なので OK
    }
    // println!("The longest string outside the scope: {}", result); // エラー! result の参照先(string3)はスコープ外

    let result2 = longest(string1.as_str(), string2); // string1 と string2 のうち短い方のライフタイム (string1) に束縛される
    println!("Another longest string: {}", result2); // OK
}

💡 <'a> はジェネリックライフタイムパラメータを宣言する部分です。関数名と引数リストの間に置きます。

この注釈により、戻り値の参照のライフタイムは、引数 xy のライフタイムのうち、短い方と同じになることが保証されます。これにより、ダングリングポインタの発生を防ぎます。

構造体が自身で所有しないデータへの参照をフィールドとして持つ場合も、ライフタイム注釈が必要です。

// コンパイルエラーになる例
// struct ImportantExcerpt {
//     part: &str,
// }

// fn main() {
//     let novel = String::from("Call me Ishmael. Some years ago...");
//     let first_sentence = novel.split('.').next().expect("Could not find a '.'");
//     let i = ImportantExcerpt {
//         part: first_sentence,
//     };
// }

上記のコードは、ImportantExcerpt 構造体の定義でライフタイムを指定していないため、コンパイルエラーになります。構造体のインスタンスが有効である限り、その中の参照 part も有効でなければなりません。

構造体定義にライフタイム注釈を追加します。

struct ImportantExcerpt<'a> {
    part: &'a str, // この参照はライフタイム 'a を持つ
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");

    // i のライフタイムは first_sentence (の元データ novel) より短くなければならない
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    println!("Excerpt part: {}", i.part);

    // ライフタイムの例
    let excerpt;
    {
        let external_string = String::from("This is an external string.");
        excerpt = ImportantExcerpt { part: external_string.as_str() };
        println!("Inside scope: {}", excerpt.part); // OK
    }
    // println!("Outside scope: {}", excerpt.part); // エラー! external_string はスコープ外
}

構造体名に続く <'a> でライフタイムパラメータを宣言し、参照フィールド part&'a str と注釈します。これにより、ImportantExcerpt のインスタンスは、その part フィールドが参照するデータよりも長く生存できないことが保証されます。

構造体にライフタイム注釈がある場合、その構造体のメソッド定義(impl ブロック)でもライフタイム名を宣言し、使用する必要があります。

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

// impl ブロックでもライフタイム 'a を宣言する必要がある
impl<'a> ImportantExcerpt<'a> {
    // 第1ライフタイム省略規則により、メソッドのライフタイムは &self のライフタイムと同じになる
    fn level(&self) -> i32 {
        3 // 仮のレベルを返す
    }

    // 戻り値が &self の一部を参照しない場合、ライフタイム注釈が必要になることがある
    // この例では、引数 announcement のライフタイム 'b が戻り値に関連付けられる
    fn announce_and_return_part<'b>(&self, announcement: &'b str) -> &'b str {
        println!("Attention please: {}", announcement);
        // self.part // もし self.part を返したいなら、戻り値のライフタイムは &'a str になる
        announcement // この場合、戻り値のライフタイムは 'b に依存する
    }
}

fn main() {
    let novel = String::from("The quick brown fox.");
    let excerpt = ImportantExcerpt { part: &novel[4..9] }; // "quick"

    println!("Level: {}", excerpt.level());

    let announcement = String::from("Important news!");
    let returned_announcement = excerpt.announce_and_return_part(&announcement);
    println!("Returned announcement: {}", returned_announcement);
}

多くの場合、メソッド定義ではライフタイム省略規則(後述)が適用されるため、明示的な注釈が不要なケースも多いです。特に &self&mut self を引数に取るメソッドでは、入力ライフタイムが出力ライフタイムに自動的に適用されることが多いです。

'static は特別なライフタイムで、プログラムの実行期間全体を表します。つまり、'static な参照はプログラムが終了するまで常に有効です。

  • 文字列リテラル ("hello") は、プログラムのバイナリに直接埋め込まれるため、自動的に 'static ライフタイムを持ちます (型は &'static str)。
  • 定数 (const) や静的変数 (static) も 'static ライフタイムを持ちます。
let s: &'static str = "I have a static lifetime.";

fn main() {
    println!("{}", s);
}

⚠️ 注意

エラーメッセージで 'static が提案されることがありますが、安易に使うべきではありません。多くの場合、それは参照元のデータが予期せず早く破棄されてしまう根本的な問題を隠蔽している可能性があります。データの所有権やスコープを再確認することが重要です。

毎回ライフタイム注釈を書くのは大変なので、Rustコンパイラにはよくあるパターンを推論するためのライフタイム省略規則が組み込まれています。これにより、多くの場合でライフタイム注釈を省略できます 🎉。

コンパイラは以下の3つの規則を順番に適用します:

  1. 第1規則 (入力ライフタイム): 参照である各引数に、それぞれ異なるライフタイムパラメータが割り当てられます。 例: fn foo(x: &i32, y: &i32)fn foo<'a, 'b>(x: &'a i32, y: &'b i32) のように扱われます。
  2. 第2規則 (単一入力ライフタイム): 入力ライフタイムパラメータがちょうど1つだけの場合、そのライフタイムが出力参照すべてに割り当てられます。 例: fn foo(x: &i32) -> &i32fn foo<'a>(x: &'a i32) -> &'a i32 のように扱われます。
  3. 第3規則 (メソッドの self): メソッドで、複数の入力ライフタイムパラメータがあり、そのうちの1つが &self または &mut self の場合、self のライフタイムが出力参照すべてに割り当てられます。これにより、メソッド呼び出しが書きやすくなります。 例: fn foo(&self, x: &i32) -> &i32fn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32 のように扱われます。

これらの規則でコンパイラがライフタイムを однозначно (ambiguityなく) 決定できない場合にのみ、明示的なライフタイム注釈が必要になります。先ほどの longest 関数の例は、第2規則も第3規則も適用できず、戻り値のライフタイムが曖昧だったため、注釈が必要でした。

👍 これらの規則のおかげで、多くの一般的なケースではライフタイム注釈を省略でき、コードを簡潔に保つことができます。

ライフタイム注釈は、Rustのメモリ安全性を保証するための重要な機能です。参照を使う際に、コンパイラがライフタイムの関係を推論できない場合に必要となります。

  • ライフタイム注釈 ('a) は、参照の有効期間の関係性をコンパイラに伝えます。
  • 関数シグネチャや構造体定義で参照を使う際に特に重要になります。
  • 'static ライフタイムはプログラム全体の期間を示しますが、注意して使用する必要があります。
  • ライフタイム省略規則により、多くの場合で注釈を省略できます。

最初は少し難しく感じるかもしれませんが、ライフタイムの概念と注釈の書き方に慣れることで、より安全で堅牢なRustコードを書けるようになります。ダングリングポインタの恐怖から解放されましょう! 💪

コメント

タイトルとURLをコピーしました