Rust のオブジェクトのライフタイム(生存期間)を理解する ('static, 'a)

ライフタイムの基本

Rust のオブジェクトのライフタイム(生存期間)は、その名の通り、オブジェクトが有効な期間を表しています。 変数とその借用 (borrow) である参照は、異なるライフタイムを持っています。

変数のライフタイム

変数のライフタイムは、その変数が初期化されるときに開始し、スコープを抜けるときに終了します。 スコープを抜けるとき、その変数はドロップ(破棄)され、それ以降は使用できなくなります。

{
  let s = String::from("Hello");
  // 変数 s のライフタイムはここで終わり、ドロップされる
}
// 変数 s はここではもう使えない

スコープはその変数を囲んでいるブロックであり、専門的には lexical scope と呼ばれます。 そのため、変数のライフタイムのことを lexical lifetime と呼ぶことがあります。

参照のライフタイム

一方で、参照(リファレンス)のライフタイムは、その参照を使う最後の文で終了します。 つまり、参照を使っているコード範囲がそのままライフタイムになるため、とてもシンプルです。

let s = String::from("Hello");
let r = &s;  // 参照 r のライフタイムはここから始まり
println!("{}", r);  // ここで終わる
println!("{}", s);

参照は、ある変数を借用 (borrow) したものであり、参照のライフタイムが終了しても、その参照先の変数が破棄(ドロップ)されるようなことはありません。

ライフタイムは「変数>参照」でなければいけない

変数とその参照のライフタイム関係は、次のような入れ子関係になっていなければいけないことは明らかです。

/p/zfhtasm/img-001.drawio.svg
図: 正しいライフタイム関係

次のように、参照のライフタイムが、その参照先のオブジェクトのライフタイムを超えるのはおかしいからです。

/p/zfhtasm/img-002.drawio.svg
図: 誤ったライフタイム関係

次のコードは、参照がライフタイム違反をしている例です。

間違った例
let r;
{
    let num = 1;
    r = #
}
println!("{}", r);  // NG! (参照先の num はすでにドロップされている)

Rust コンパイラーは内部に borrow checker という仕組みを備えており、上記のような不正なライフタイム関係がないかを確認してくれます。 上記のコードをコンパイルしようとすると、次のようなコンパイルエラーになります。

error[E0597]: `num` does not live long enough

このようなシンプルな例であれば、ライフタイム違反をしていることは簡単に分かりますが、関数の戻り値として参照を返す場合や、構造体のフィールドとして参照を持つような場合は、若干複雑になってきます。 以下、これらを順番に見ていきます。

関数の戻り値を参照にする

関数の戻り値として参照を返す場合、次のような種類の参照を返すことが考えられます。

  • 定数オブジェクト(リテラル)の参照
  • 引数として渡された参照に依存する参照

それぞれ、参照先のオブジェクトのライフタイムが、どのように戻り値の参照に影響するかをコンパイラに伝えてやる必要があります。

定数オブジェクト(リテラル)の参照を返す場合

文字列リテラルや数値リテラルなど、プログラムの実行時間とライフタイムが等しいデータを参照として返す場合はシンプルです。 参照を表す & の代わりに、&'static を付けてやれば OK です。 アポストロフィー (') は、それが ライフタイム識別子 (lifetime specifier) であることを示しています。

/// 文字列リテラルの参照を返す関数
fn get_str_ref() -> &'static str { "Hello, world!" }

/// 数値リテラルの参照を返す関数
fn get_f64_ref() -> &'static f64 { &1.234 }

/// 定数(数値)の参照を返す関数
const NUM: i32 = 777;
fn get_const_ref() -> &'static i32 { &NUM }

/// 定数(配列)の参照を返す関数
const ARRAY: [i32; 3] = [1, 2, 3];
fn get_array_ref() -> &'static [i32; 3] { &ARRAY }

// 使用例
let s = get_str_ref();    //=> &str ("Hello, world!")
let f = get_f64_ref();    //=> &f64 (1.234)
let i = get_const_ref();  //=> &i32 (777)
let a = get_array_ref();  //=> &[i32; 3] ([1, 2, 3])
println!("{:?}, {:?}, {:?}, {:?}", s, f, i, a);

上記の例では、説明のためにすべて参照で返していますが、単純な数値のスカラー値 (i32f64) であれば、参照ではなく値として返した方がシンプルです。

引数で渡された参照に依存する参照を返す場合

関数の引数として渡された参照のライフタイムに依存する参照を戻り値として返す場合、そのライフタイムの関係を意識する必要があります。 次のように、参照のパラメーターが 1 つだけの場合、Rust は戻り値の参照もそれと同じライフタイム内で有効である判断してくれるので、問題なくコンパイルできます。

/// 文字列スライスの最初の n 文字を返す
fn first_n_chars(text: &str, n: usize) -> &str {
    &text[..n]
}

// 以下の参照 r は s のライフタイム内でのみ有効
let s = String::from("ABCDEF");
let r = first_n_chars(s.as_str(), 3);
println!("{:?}", r);  //=> "ABC"
☝️ 一時インスタンスは渡せない

上記の first_n_chars 関数を次のように呼び出すと、コンパイルエラーになります。

let r = first_n_chars(String::from("ABCDEF").as_str(), 3);  // エラー!
println!("{:?}", r);  // ここに到達する時点ですでに r は無効

なぜなら、String インスタンス ("ABCDEF") のライフタイムが first_n_chars を呼び出している行で終了してしまい、戻り値の参照もその行までしか有効でないからです。 String インスタンスは変数で保持するようにして、その変数が所属するブロックの末尾までライフタイムを伸ばしてやる必要があります。

関数の引数として参照が 2 つ以上渡される場合は、そのまま次のように書くとコンパイルエラーになってしまいます。

fn longest(x: &str, y: &str) -> &str {  // エラー!
    if x.len() > y.len() { x } else { y }
}

なぜなら、戻り値の参照の有効期間が、参照 x によって決まるものなのか、参照 y によって決まるものなのかを判断できないからです。 Rust コンパイラーは、「戻り値の参照の有効期間を関数のシグネチャから判断できること」を要求します。

上記コードをコンパイルできるようにするには、次のようなライフタイム識別子 'a を付加します。 ライフタイム識別子は、ジェネリクスの型パラメーター (<T>) と同様の形式で宣言しますが、名前をアポストロフィー (') で始めて、小文字を使うところが異なります。 通常は、'a'b のような短い名前を使います。

/// 渡された文字列のうち長い方を返す
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// 使用例
let s1 = "AAA";
let s2 = String::from("BBBBB");
let s3 = longest(s1, &s2);  //=> "BBBBB"

この longest 関数のシグネチャは、「戻り値の参照は、参照 x と参照 y の両方が有効な期間だけ有効である」 ことをコンパイラーに伝えています。 言い換えると、「戻り値の参照は、xy のライフタイムのうち、短い方の期間だけ有効である」ということです。

上記の使用例では、s1s2(の文字列スライス)を引数として渡していますが、戻り値の参照の有効期間は、実質 s2 のライフタイムによって決まります。 s1 の方は、静的な文字列リテラルへの参照であり、明らかに s2 よりもライフタイムが長い(というより死なない)からです。

構造体のフィールドとして参照を持つ

参照フィールドには必ずライフタイムが必要

構造体のフィールドとして参照を持つ場合、ライフタイム識別子の指定が必要です。 次の Excerpt 構造体は、外部テキストの部分テキスト(の参照)を保持する構造体です。

// 参照フィールドを持つ構造体
struct Excerpt<'a> {
    part: &'a str,
}

// 使用例
let text = String::from("ABCDEFGHIJKLMNOPQRSTUVWXY");
let excerpt = Excerpt { part: &text[..5] };
println!("{:?}", excerpt.part);  //=> "ABCDE"

このライフタイム指定 ('a) は、Excerpt のインスタンスは part フィールドのライフタイム内でのみ有効 であることを示しています。 外部のテキストの一部を参照するわけですから、この関係性はすんなり理解できると思います。

上記の使用例では、他の変数 (text) が所有している文字列の中の、先頭 5 文字の部分テキストを excerpt インスタンス内に保持しています。 excerpt インスタンスは、text 変数のライフタイム内でのみ有効です。

ちなみに、フィールド名を持たないタプル構造体の場合も同様です。 下記のタプル構造体は、最初のフィールドが参照になっているため、ライフタイム識別子が必要です。

// 参照フィールドを持つタプル構造体
struct Excerpt<'a>(&'a str, i32, i32);

let text = String::from("ABCDEFGHIJKLMNOPQRSTUVWXY");
let excerpt = Excerpt(&text[..5], 0, 4);
println!("{:?}", excerpt.0);  //=> "ABCDE"

構造体のメソッドを実装する場合

ライフタイム識別子を持つ構造体のメソッドを impl ブロックで実装する場合、その書き出しは次のようになります。 ライフタイム識別子まで含めて構造体の型なので、このように記述する必要があります。

impl<'a> Excerpt<'a> {
    // ...
}

インスタンスメソッドの戻り値が参照の場合、その参照はデフォルトで、自分自身のインスタンス (self) のライフタイム内で有効とみなされます。 以下の first_n_chars メソッドは、自身が保持する part フィールドの部分文字列の参照を返していますが、このようなメソッドを定義する場合、ライフタイムの指定は省略することができます。

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

impl<'a> Excerpt<'a> {
    // 戻り値の参照は、デフォルトで構造体インスタンスが生きている間だけ有効
    fn first_n_chars(&self, n: usize) -> &str {
        &self.part[..n]
    }
}

// 使用例
let text = String::from("ABCDEFGHIJKLMNOPQRSTUVWXY");
let excerpt = Excerpt { part: &text[..5] };
println!("{:?}", excerpt.first_n_chars(3));  //=> "ABC"

大体はこのパターンになるので、ほとんどのケースでは、構造体のインスタンスメソッドが参照を返すときにライフタイムの指定は必要ありません。 もちろん、構造体インスタンス自身のライフタイムと関係ない参照を返す場合は、次のようにライフタイムの指定が必要になります。

struct Switch {
    state: bool,
}

impl Switch {
    fn a_or_b<'a>(&self, a: &'a str, b: &'a str) -> &'a str {
        if self.state { a } else { b }
    }
}