7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

所有権を「完全に理解した」と思った次の瞬間、こいつが襲ってきました。

ライフタイム(Lifetime)

'a ってなに...?」「エリジョンって何...?」「なんでコンパイル通らないの...?」

そんな状態で土日をまるごと溶かしました。かなしみ。

でも乗り越えた今なら言えます。ライフタイムは怖くない(多分)。

目次

  1. ライフタイムとは何か
  2. ライフタイム注釈が必要な場面
  3. ライフタイムエリジョン
  4. 構造体とライフタイム
  5. 複数のライフタイム
  6. 'static ライフタイム
  7. よくあるエラーと対処法
  8. まとめ

ライフタイムとは何か

ライフタイム = 参照が有効な期間

すべての参照にはライフタイムがあります。普段は明示しなくてもコンパイラが推論してくれますが、推論できない場合は自分で書く必要があります。

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 は「ライフタイムパラメータ」と呼ばれます。意味としては:

xy の両方が有効な期間 '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

原因: 参照先が先に死ぬ

対処法:

  1. 参照先のスコープを広げる
  2. 所有権を渡す(Clone するなど)
  3. 設計を見直す

❌ "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,
}

複雑なライフタイム地獄に陥りそうなら、StringVec を使って所有権を持たせるのも手です。パフォーマンスより可読性が大事な場面も多い。

まとめ

ライフタイムのチェックリスト

  • 参照を返す関数には、ライフタイム注釈が必要かもしれない
  • 参照を持つ構造体には、ライフタイムパラメータが必要
  • エリジョンルールを知っておくと、省略できる場面がわかる
  • 'static は安易に使わない
  • 困ったら所有権を持たせる選択肢も考える

今すぐできるアクション

  1. エラーメッセージを読む - Rustのライフタイムエラーは解決策も提示してくれることが多い
  2. シンプルな例から始める - 複雑な構造体より、単純な関数で練習
  3. 無理に参照を使わない - String を使って所有権を持たせても良い

ライフタイム、最初は本当に意味がわからなかったけど、「参照が有効な期間を明示するもの」と理解したら少し楽になりました。

土日は溶けたけど、その価値はあったと思います...たぶん。

みなさんも頑張ってね。こわいですねぇ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?