•12 min read

Rustのライフタイムを理解する

Rustのライフタイムを理解する

はじめに

Rustは、ガベージコレクタなしでメモリ安全性を実現できることでよく評価されます。この安全性の保証の中核にあるのが「所有権(ownership)」という概念であり、所有権と密接に結びついているのが「ライフタイム(lifetimes)」です。Python、Java、C#のような自動メモリ管理を持つ言語や、C、C++のような手動メモリ管理を持つ言語から来た多くの開発者にとって、ライフタイムは理解するのが最も難しい概念の一つかもしれません。

この包括的なガイドでは、ライフタイムとは何か、なぜ存在するのか、そしてRustプログラムでライフタイムを効果的に扱う方法を詳しく説明します。この記事を読み終える頃には、この基礎的な機能についてしっかりとした理解が得られるはずです。

Audio Briefing
0:00 / 0:00

ライフタイムとは?

最も簡単に言えば、ライフタイムとは、すべての借用が有効であることを保証するためにRustコンパイラが使用する構成要素です。Rustのすべての参照にはライフタイムがあり、それはその参照が有効であるスコープです。ほとんどの場合、ライフタイムは型が推論されるのと同じように、暗黙的かつ推論されます。しかし、参照を使用する関数や構造体がある場合、異なる参照のライフタイムが互いにどのように関連しているかをコンパイラに伝えるために、これらのライフタイムを注釈付けする必要があることがよくあります。

ライフタイムの主な目的は、「ダングリング参照(dangling references)」を防ぐことです。ダングリング参照は、プログラムがすでに解放またはドロップされたデータにアクセスしようとしたときに発生します。C++では、これはしばしば未定義の動作、セグメンテーション違反、またはセキュリティ脆弱性につながります。Rustのコンパイラ(rustc)は、借用チェッカー(borrow checker)を使用して、すべての借用が有効であり、どの参照もそれが指すデータよりも長く存続しないことを保証します。

借用チェッカー

ライフタイムを理解するには、まず借用チェッカーを理解する必要があります。借用チェッカーは、すべての借用が有効であるかどうかを判断するためにスコープを比較します。借用チェッカーが防ぐ典型的な例を見てみましょう。

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

このコードでは、r はライフタイム 'a を持ち、x はライフタイム 'b を持ちます。内部ブロックは x を作成し、x への参照を r に代入しようとします。内部ブロックが終了すると、x はドロップされ、スコープ外になります。しかし、r はまだスコープ内にあり、x が以前あったメモリを指そうとしています。借用チェッカーはこれを見て、「エラー: x は十分に長く生存しません。」と報告します。ライフタイム 'b は 'a よりも短いため、参照は無効です。

Advertisement

明示的なライフタイムアノテーション

コンパイラは多くの単純なケースでライフタイムを推論するのに十分賢いですが、あなたの助けが必要な場合があります。関数が複数の参照を受け取る場合や参照を返す場合、返される参照のライフタイムが入力参照のライフタイムとどのように関連しているかを知る必要があります。

2つの文字列スライスの中から長い方を返す関数を考えてみましょう。

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

これをコンパイルしようとすると、Rustはエラーをスローします: missing lifetime specifier。コンパイラは、返される参照が x を指すのか y を指すのかを知りません。x と y は異なるライフタイムを持つ可能性があるため、コンパイラはどちらが戻り値のライフタイムを決定するのかを知る必要があります。

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

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

ここでは、山括弧 <'a> の中にライフタイムパラメータ 'a を宣言しています。そして、パラメータ x と y、および戻り値の型にこのライフタイムを注釈付けします。これは借用チェッカーに次のように伝えます。「関数 longest は、あるライフタイム 'a と同じくらい長く生存する2つの参照を受け取り、同じく 'a と同じくらい長く生存する参照を返します。」

実際には、これは返される参照が x と y の両方が有効である限り有効であることを意味します。借用チェッカーは、戻り値のライフタイムを引数のライフタイムの短い方に制限します。

ライフタイム省略

あらゆる場所にライフタイムを記述すると、Rustコードは非常に冗長になります。これを軽減するために、Rustチームは「ライフタイム省略規則(lifetime elision rules)」をコンパイラに実装しました。これらは、関数シグネチャでライフタイムを推論するためにコンパイラが従う3つの簡単な規則です。

  1. 入力位置で省略された各ライフタイムは、異なるライフタイムパラメータになります。
  2. 入力ライフタイム位置がちょうど1つだけある場合(省略されているかどうかにかかわらず)、そのライフタイムが省略されたすべての出力ライフタイムに割り当てられます。
  3. 複数の入力ライフタイム位置があるが、そのうちの1つが &self または &mut self である場合(メソッドの場合など)、self のライフタイムが省略されたすべての出力ライフタイムに割り当てられます。

コンパイラがこれらの規則を適用しても、出力参照のライフタイムを決定できない場合、エラーをスローし、手動で注釈を付けることを強制します。

構造体とライフタイム

ライフタイムは関数に限定されません。構造体が参照を保持する場合、ライフタイムで注釈付けする必要があります。

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("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

ここでは、ImportantExcerpt は文字列スライスへの参照を保持しています。ライフタイムアノテーション 'a は、ImportantExcerpt のインスタンスが、その part フィールドに保持する参照よりも長く存続できないことを保証します。もし novel 変数が ImportantExcerpt インスタンスよりも先にドロップされた場合、コンパイラはそのコードのコンパイルを阻止します。

静的ライフタイム

知っておくべき特別なライフタイムが1つあります: 'static。'static ライフタイムは、参照がプログラムの全期間にわたって存続できることを意味します。すべての文字列リテラルは 'static ライフタイムを持ちます。

let s: &'static str = "I have a static lifetime.";

参照に 'static ライフタイムを持たせることはできますが(例えば、Box::leak を使ってメモリリークさせるなど)、控えめに使用すべきです。ほとんどの場合、コンパイラが 'static バウンドの追加を提案するときは、データが永遠に存続する必要があるというよりも、所有権に問題がある兆候です。

Advertisement

まとめ

ライフタイムはRust独自の概念であり、ガベージコレクタなしで強力なメモリ安全性の保証を可能にします。借用チェッカーがスコープをどのように分析するか、明示的なライフタイムアノテーションをどのように使用するか、そしてコンパイラが省略を通じてライフタイムをどのように推論するかを理解することで、安全で効率的かつ表現力豊かなRustコードを書くことができます。

初めてライフタイムに遭遇したときはイライラするかもしれませんが、やがては自然に身につくでしょう。ライフタイムは、データのメモリレイアウトと寿命について慎重に考えることを強制し、最終的にはあなたをより良いプログラマーにします。練習を続け、コンパイラのエラーメッセージを読むことを恐れないでください(それらは信じられないほど役立ちます!)、そしてすぐにあなたは自分のプロジェクトでライフタイムをマスターするでしょう。

こちらもおすすめです

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement