🙆

RustのLifetimeってなんなん

2021/06/19に公開

概要

Rustチュートリアル読んでてOwnershipの次に「むむ」ってなる
Lifetimeにたどり着いたので頭の中を整理してみる

Lifetimeってなんなん?

&を用いた参照(Reference)を利用した際に
Dangling Referenceが発生しないように利用する概念みたい
(Dangling Reference:解放されたメモリを参照してしまう状態)

順を追ってチュートリアルを噛み砕いてみる

Dangling Referenceになるコード

たとえばこれ
もう解放されたxの値(メモリ)を参照しようとしてDangling Reference状態

fn main() {
    let r; 

    {
        let x = 5;
        r = &x;
    }  // xがdropされる

    println!("r is {}", r); // dropされたxを参照してるr君
}

どうやってDangling Referenceを判定してるの?

RustのコンパイラにはBorrow Checkerなる機能があるらしく
こいつがLifetimeっていう概念を用いて、Dangling Referenceを判定してるみたい

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

上記コメントに記載しましたが
変数rは'aで示した期間(Lifetime)有効である。長いね
変数xは'bで示した期間(Lifetime)有効である。短いね

コンパイル時に、
「rがxを参照してるぞ。でも参照してる'bのLifetimeは自分の'a Lifetimeより短いやんけ!エラー!」
とそれぞれの長さを見て判定してるらしい。

このBorrow CheckerによるLifetime判定の影響で
Rustでは自分で明示的にLifetimeをannotationしてあげないとコンパイルエラーになるケースがあるみたい。

Lifetimeのannotationを付与する

関数

コンパイルエラーになっちゃうケース

この関数はまさにコンパイルエラーが発生しちゃう

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// 戻り値の参照がどちらの引数の参照をしてるか、コンパイル時に判断不能
// なので、Borrow CheckerがDangling Referenceのチェックができないのよ・・・
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

解決方法

引数と戻り値にLifetime Annotationをつけてあげる
ジェネリクスの記法に準じて、関数に<'a>を渡して
引数と戻り値にそれぞれ&'aこんな感じでannotationをつけてあげるとコンパイルエラーから抜けることができる!

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// Lifetime Annotationつけてあげる
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

明示的に記述したLifetime'a
x, yの引数のうちLifetimeが短い方のLifetimeと等しい扱いになるみたいです

短い方のLifetimeってどう言うこと?

これがコンパイルエラーになっちゃう感じ

コメントした通り、短い方のlifetimeを見ちゃって
Borrow CheckerがDangling Referneceの可能性を考慮してコンパイルエラーにするのだ

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
	// resultはstring1なので
        result = longest(string1.as_str(), string2.as_str());
    }
    // スコープ抜けたここでも有効なはずだけど
    println!("The longest string is {}", result);
}

// `aのlifetimeは短い方のstring2を用いて、Borrow Checkerがチェックしてる
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

struct

コンパイルエラーになっちゃうケース

構造体のフィールドに参照がある場合、
lifetime annotationが指定されてないとコンパイルエラーになるらしい

#[derive(Debug)]
struct ImportantExcerpt {
    part: &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 };
    println!("ImportantExcerpt is {:?}", i)   
}

解決方法

lifetime annotationをつけてあげるのさ!

#[derive(Debug)]
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 };
    println!("ImportantExcerpt is {:?}", i)
}

Lifetimeのannotationが省略できるケース

全ての参照にLifetimeがあるが、明記しなくてもいいケースがあるらしい
昔(Rust1.0以前)は全部明記しなきゃいけなかったらしいけど
明らかにLifetimeの予測が可能な場合は省略できる様になったらしい

具体的には以下3つの規則に準じて、
Borrow Checkerが戻り値のLifetime判別できる場合省略可能

  1. 参照の各引数は、独自のライフタイム引数を得る
  2. 1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入される
  3. selfのあるメソッドの場合、selfのライフタイムが全出力ライフタイム引数に代入される

省略できる例(メソッド)

fn first_word(s: &str) -> &str {

規則1に準じて引数に独自lifetime annotationを付与
fn first_word<'a>(s: &'a str) -> &str {

規則2に準じて引数ひとつなので、戻り値に引数のlifetimeが指定される
fn first_word<'a>(s: &'a str) -> &'a str {

戻り値のlifetime判明してるので省略可能じゃ!

省略できない例(関数)

fn longest(x: &str, y: &str) -> &str {

規則1に準じて各引数に独自lifetime annotationを付与
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

規則2も3も適用できない・・・
戻り値のlifetime判明しない・・・ので明示的に設定が必要

省略できる例(メソッド)

メソッドにはこんな感じでジェネリクス形式のlifetime annotation指定できるらしい

impl<'a> ImportantExcerpt<'a> {
    // 規則1に準じて戻り値のlifetimeわかるからannotationいらず
    fn level(&self) -> i32 {
        3
    }
}

&selfがいるので3番目の規則に準じて
&selfのlifetimeが戻り値のlifetimeに適用される。annotationいらず!

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

structのメソッドはselfあることが多そうだし、結構省略できるんだなきっと🧐

静的lifetime

&'staticこんなlifetime指定を静的lifetimeていうらしい。
プログラムの全期間を指すlifetimeらしい

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

staticを指定しなさい!みたいなメッセージが出る時あるらしいけど
スコープは基本必要な時だけ、短い方が適切なケースがほとんどだろうし
むやみやたらにstaticつけるのはやめてちゃんと考えましょうね。ってことらしい

まとめ

チュートリアルのコピペみたいな内容になってしまった🙈

要は参照渡して参照返すとき、コンパイラがlifetimeわからなくなるケースがあるから
明示的に書いてあげるべきケースがあるってことみたい。

コンパイラに怒られたら、追記してあげるくらいの気持ちでいよう。

Discussion