ようこそ、Rustの学習における「最難関」とも呼ばれる章へ。 これまで所有権や借用(第4章、第5章)を学んできましたが、ライフタイム(Lifetimes) はそれらを支えるコンパイラのロジックそのものです。
他の言語(C/C++)では、メモリが解放された後にその場所を指し続ける「ダングリングポインタ」がバグの温床でした。JavaやPythonのようなガベージコレクション(GC)を持つ言語では、これを自動で管理しますが、パフォーマンスのコストがかかります。
Rustは「コンパイル時に参照の有効性を厳密にチェックする」ことで、GCなしでメモリ安全性を保証します。そのための仕組みがライフタイムです。
ライフタイムとは、簡単に言えば「その参照が有効である期間(スコープ)」のことです。
実は、これまでの章でもあなたは無意識にライフタイムを使用してきました。通常、コンパイラが自動的に推論してくれるため、明示する必要がなかっただけです。しかし、コンパイラが「参照の有効期間が不明瞭だ」と判断した場合、プログラマが明示的に注釈(アノテーション)を加える必要があります。
ライフタイムの主な目的は、無効なデータを指す参照を作らせないことです。
以下のコードを見てください(これはコンパイルエラーになります)。
{
let r; // ---------+-- rのライフタイム
// |
{ // |
let x = 5; // -+-- xのライフタイム
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
ここで、r は x を参照しようとしています。しかし、内側のブロック {} が終わった時点で x は破棄されます。その後に r を使おうとすると、r は「解放されたメモリ」を指していることになります。
Rustのコンパイラ(借用チェッカー)は、このスコープのズレを検知し、「x の寿命が短すぎる」としてエラーを出します。
最も頻繁にライフタイム注釈が必要になるのは、「引数として参照を受け取り、戻り値として参照を返す関数」です。
2つの文字列スライスを受け取り、長い方を返す関数 longest を考えてみましょう。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}このコードをコンパイルしようとすると、以下のようなエラーが出ます。
error[E0106]: missing lifetime specifier
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`なぜエラーになるのか?
コンパイラには、longest 関数が x を返すのか y を返すのか実行時まで分かりません。そのため、戻り値の参照がいつまで有効であれば安全なのか(xの寿命に合わせるべきか、yの寿命に合わせるべきか) を判断できないのです。
ここでジェネリックなライフタイム注釈が登場します。
構文は 'a のようにアポストロフィから始まる名前を使います。通常は 'a(a, b, c...)が使われます。
注釈のルールは以下の通りです:
<'a> でライフタイムパラメータを宣言する。&'a str のように付与する。修正したコードがこちらです。
// 'a というライフタイムを宣言し、
// 「引数x、引数y、そして戻り値は、すべて少なくとも 'a と同じ期間だけ生きている」
// という制約をコンパイラに伝える。
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("long string is long");
let result;
{
let string2 = String::from("xyz");
// resultのライフタイムは、string1とstring2のうち「短い方」の寿命に制約される
result = longest(string1.as_str(), string2.as_str());
println!("The longest string is '{}'", result);
}
// ここで string2 がドロップされるため、result も無効になる。
// もしここで result を使おうとするとコンパイルエラーになる(安全!)。
// println!("The longest string is '{}'", result);
}The longest string is 'long string is long'
重要なポイント:
ライフタイム注釈 'a は、変数の寿命を延ばすものではありません。
「複数の参照の寿命の関係性」をコンパイラに説明し、「渡された参照の中で最も寿命が短いもの」 に戻り値の寿命を合わせるように制約するものです。
「待ってください。第4章で書いた fn first_word(s: &str) -> &str は注釈なしで動きましたよ?」
鋭い質問です。初期のRustではすべての参照に明示的なライフタイムが必要でした。しかし、あまりにも頻出するパターン(例えば「引数が1つなら、戻り値のライフタイムもそれと同じ」など)があったため、Rustチームはそれらを自動推論する「ライフタイム省略ルール(Lifetime Elision Rules)」をコンパイラに組み込みました。
コンパイラは以下の3つのルールを順番に適用します。それでもライフタイムが決まらない場合のみ、エラーを出して人間に注釈を求めます。
fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)fn foo<'a>(x: &'a str) -> &'a strfirst_word 関数で注釈が不要だった理由です。&self または &mut self を含む)の場合、self のライフタイムをすべての出力に割り当てる
これまでの章では、構造体には String や i32 などの「所有される型」を持たせてきました。
しかし、構造体に参照を持たせたい場合もあります。その場合、「構造体そのものよりも、中の参照先が長生き(あるいは同等の寿命)である」ことを保証しなければなりません。
// ImportantExcerpt構造体は、'a という期間だけ生きる文字列スライスを保持する
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 '.'");
// 構造体のインスタンスを作成
// part は novel の一部を参照している。
// novel が有効である限り、i も有効であることが保証される。
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Novel start: {}", i.part);
}Novel start: Call me Ishmael
もし User 構造体のような定義で <'a> を忘れると、「参照を持たせるならライフタイムを指定せよ」というエラーになります。これは、構造体が生きている間に参照先のデータが消えてしまうのを防ぐためです。
特別なライフタイムとして 'static があります。
これは、「参照がプログラムの実行期間全体にわたって有効である」ことを意味します。
すべての文字列リテラルは 'static ライフタイムを持っています。なぜなら、それらはプログラムのバイナリ自体に埋め込まれており、メモリ上の位置が固定されているからです。
let s: &'static str = "I have a static lifetime.";
注意点:
エラーメッセージで「'static が必要です」と提案されることがありますが、安易に 'static を使って解決しようとしないでください。多くの場合、それは「参照ではなく所有権を持つべき」か「ライフタイムの関係を正しく記述すべき」場面であり、本当にプログラム終了までデータを保持し続けたいケースは稀です。
<'a> のようなジェネリックライフタイム注釈が必要です。ライフタイムの記法は最初は「ノイズ」に見えるかもしれませんが、これは「メモリ安全性をコンパイラとプログラマが対話するための言語」です。これを理解すれば、C++のような複雑なメモリ管理の落とし穴を完全に回避できます。
以下の要件を満たすコードを作成してください。
Book という構造体を定義してください。title というフィールドを持ち、それは String ではなく文字列スライス &str です(ライフタイム注釈が必要です)。main 関数で String 型の変数(例: "The Rust Programming Language")を作成し、Book のインスタンスにその参照を渡してください。Book のインスタンスを表示してください(Debug トレイを導出(#[derive(Debug)])して構いません)。// ここにBookの定義を書いてください
fn main() {
let book_title = String::from("The Rust Programming Language");
let my_book = Book {
title: &book_title,
};
println!("Book details: {:?}", my_book);
}Book details: Book { title: "The Rust Programming Language" }以下の要件を満たすコードを作成してください。
str1, str2 を受け取る関数 first_word_of_longer を作成してください。'a を付けてください。main 関数で動作確認をしてください。(ヒント: 単語の切り出しは s.split_whitespace().next() などが使えますが、戻り値のライフタイムが引数と紐付いていることが重要です)
// ここにfirst_word_of_longer関数を書いてください
fn main() {
let string1 = String::from("Hello World from Rust");
let string2 = String::from("Hi");
let first_word = first_word_of_longer(string1.as_str(), string2.as_str());
println!("The first word of the longer string is: '{}'", first_word);
}The first word of the longer string is: 'Hello'