はじめに
所有権を「完全に理解した」と思った次の瞬間、こいつが襲ってきました。
ライフタイム(Lifetime)
「'a ってなに...?」「エリジョンって何...?」「なんでコンパイル通らないの...?」
そんな状態で土日をまるごと溶かしました。かなしみ。
でも乗り越えた今なら言えます。ライフタイムは怖くない(多分)。
目次
ライフタイムとは何か
ライフタイム = 参照が有効な期間
すべての参照にはライフタイムがあります。普段は明示しなくてもコンパイラが推論してくれますが、推論できない場合は自分で書く必要があります。
fn main() {
let r; // --------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
// println!("r: {}", r); // ここでrを使うと、xはすでに死んでいる
} // --------+
x のライフタイム 'b より r のライフタイム 'a の方が長いので、ダングリング参照(無効な参照)になってしまう。Rustはこれをコンパイル時に検出します。
ライフタイム注釈が必要な場面
関数で参照を返すとき
// コンパイルエラー!
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
このエラーメッセージ:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
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`
「返り値の参照が x から来たのか y から来たのかわからないよ」 と言われています。
解決策:ライフタイム注釈をつける
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
'a は「ライフタイムパラメータ」と呼ばれます。意味としては:
「
xとyの両方が有効な期間'aの間、返り値も有効」
使用例
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
The longest string is abcd
ライフタイムエリジョン
「え、でも今まで &str を返す関数書いたことあるけど、'a とか書かなかったよ?」
それは ライフタイムエリジョン(Lifetime Elision) のおかげです。
3つのエリジョンルール
コンパイラは以下のルールで自動的にライフタイムを推論します:
ルール1: 入力ライフタイム(引数の参照)にはそれぞれ別のライフタイムが割り当てられる
fn foo(x: &str, y: &str)
// ↓ 内部的にはこう解釈
fn foo<'a, 'b>(x: &'a str, y: &'b str)
ルール2: 入力ライフタイムが1つだけなら、それが出力ライフタイムに適用される
fn first_word(s: &str) -> &str
// ↓ 内部的にはこう解釈
fn first_word<'a>(s: &'a str) -> &'a str
ルール3: メソッドで &self か &mut self があれば、そのライフタイムが出力に適用される
impl Foo {
fn method(&self, x: &str) -> &str
// ↓ 内部的にはこう解釈
fn method<'a, 'b>(&'a self, x: &'b str) -> &'a str
}
エリジョンが効かない例
fn longest(x: &str, y: &str) -> &str // ルール適用後もライフタイム不明
入力が2つあるので、ルール2は適用できない。だから自分で書く必要があります。
構造体とライフタイム
構造体が参照を持つ場合も、ライフタイム注釈が必要です。
// コンパイルエラー!
struct ImportantExcerpt {
part: &str,
}
// OK
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
}
ImportantExcerpt<'a> は「'a の間、この構造体は有効」という意味。
よくある間違い
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let excerpt;
{
let novel = String::from("Call me Ishmael.");
excerpt = ImportantExcerpt {
part: &novel, // novelのライフタイムはこのブロック内
};
} // novelがドロップされる
// println!("{}", excerpt.part); // ダングリング参照!
}
複数のライフタイム
場合によっては、異なるライフタイムを使い分けることがあります。
fn longest_with_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
ann は返り値に影響しないので、別のライフタイム 'b を使っています。
ライフタイムの関係を指定
fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b は 'a より長生き
{
x
}
'b: 'a は「'b は 'a より長いか同じ」という制約です。
'static ライフタイム
'static は特殊なライフタイムで、「プログラム全体で有効」を意味します。
let s: &'static str = "I have a static lifetime.";
文字列リテラルはバイナリに埋め込まれるので、プログラム実行中ずっと有効です。
注意:安易に 'static を使わない
エラーメッセージで「'static が必要」と言われても、安易に 'static にしないでください。
// 悪い例:'static で逃げる
fn get_str() -> &'static str {
let s = String::from("hello");
&s // コンパイルエラー!sはこの関数内でしか生きていない
}
'static は「本当にプログラム全体で生きている値」にだけ使うべきです。
'static が適切な場面
// 定数
const MESSAGE: &'static str = "Hello, world!";
// lazy_staticやonce_cellで初期化される値
use once_cell::sync::Lazy;
static CONFIG: Lazy<String> = Lazy::new(|| {
std::fs::read_to_string("config.txt").unwrap()
});
よくあるエラーと対処法
❌ "missing lifetime specifier"
error[E0106]: missing lifetime specifier
原因: 参照を返す関数でライフタイムが指定されていない
対処法: ライフタイム注釈を追加する
// Before
fn get_first(data: &[String]) -> &String { &data[0] }
// After
fn get_first<'a>(data: &'a [String]) -> &'a String { &data[0] }
❌ "borrowed value does not live long enough"
error[E0597]: `x` does not live long enough
原因: 参照先が先に死ぬ
対処法:
- 参照先のスコープを広げる
- 所有権を渡す(Clone するなど)
- 設計を見直す
❌ "lifetime may not live long enough"
error: lifetime may not live long enough
原因: ライフタイムの関係が合わない
対処法: ライフタイム境界 where 'b: 'a を追加するか、設計を見直す
実践的なパターン
パターン1:データを借用する構造体
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, position: 0 }
}
fn current_char(&self) -> Option<char> {
self.input.chars().nth(self.position)
}
}
パターン2:イテレータを返すメソッド
struct Words<'a> {
text: &'a str,
}
impl<'a> Words<'a> {
fn iter(&self) -> impl Iterator<Item = &'a str> {
self.text.split_whitespace()
}
}
パターン3:ライフタイムを避ける設計
場合によっては、所有権を持たせた方がシンプルになることも:
// 参照を持つ(ライフタイム必要)
struct RefBased<'a> {
data: &'a str,
}
// 所有権を持つ(ライフタイム不要)
struct OwnedBased {
data: String,
}
複雑なライフタイム地獄に陥りそうなら、String や Vec を使って所有権を持たせるのも手です。パフォーマンスより可読性が大事な場面も多い。
まとめ
ライフタイムのチェックリスト
- 参照を返す関数には、ライフタイム注釈が必要かもしれない
- 参照を持つ構造体には、ライフタイムパラメータが必要
- エリジョンルールを知っておくと、省略できる場面がわかる
-
'staticは安易に使わない - 困ったら所有権を持たせる選択肢も考える
今すぐできるアクション
- エラーメッセージを読む - Rustのライフタイムエラーは解決策も提示してくれることが多い
- シンプルな例から始める - 複雑な構造体より、単純な関数で練習
-
無理に参照を使わない -
Stringを使って所有権を持たせても良い
ライフタイム、最初は本当に意味がわからなかったけど、「参照が有効な期間を明示するもの」と理解したら少し楽になりました。
土日は溶けたけど、その価値はあったと思います...たぶん。
みなさんも頑張ってね。こわいですねぇ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!