ライフタイムについて

ライフタイムについて

Validating References with Lifetimes を読んだときのメモ。

Preventing Dangling References with Lifetimes

Rustのライフタイムの主目的は、 dangling reference を防ぐこと。

fn main () {
    let r;
    {
        let x = 5;
        r = &x;
    }// (1)
    println!("r: {r}");
}

The Borrow Checker

上記のコードはコンパイルできない。仮にコンパイル出来てしまうと、 (1) でxはdeallocateされるので、 (1) 以降でrを使った処理は、正常に動作しないことになる。これを防ぐために、Rustのコンパイラはborrow checkerという機能を使って コンパイル時に上記を不正なコードと判断してくれる。

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

borrow checker が上記のように 貸した側借りた側 の変数のライフタイムの違いを検出し、 問題があればコンパイルエラーにしてくれる。

Generic ifetimes in Functions

以下はコンパイルが通らない。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

引数 x , y ライフタイムと、返却値のライフタイムの関係性が定まらないため。

Lifetime Annotation Syntax

ライフタイムアノテーションの書き方:

&'a i32

参照の & の右に 'a を付ける。(aの部分はなんでも良いが慣習としてaが使われる)

Lifetime Annotations in Function Signatures

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

上のようにライフタイムパラメータと付けることで、 「 x, y のうち、短い方のライフタイムと戻り値のライフタイムが同じになる」 とコンパイラに伝えることになる。 あくまでコンパイラにそう伝えるだけで、実際の変数のライフタイムが変わるわけではない。 この情報を参照して、 borrow checker はライフタイムの検査を行ってくれる。

付与したライフタイムパラメータのライフタイムと、実際の戻り値のライフタイムと異なるエラーになる。

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

人間であれば resultprintln! まで生きてることが見て分かるが、 コンパイラはあくまでライフタイムパラメータで示した取り決めに反してないかどうかしか分からない。

Thinking in Terms of Lifetimes

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

関数の戻り値が参照の場合、戻り値のライフタイムパラメータは、変数のいずれかのライフタイムパラメータと一致する必要がある。 上記のコードでyは、xと戻り値のライフタイムに関係が無いのでyにライフタイムパラメータを付ける必要はない。

仮に、関数の戻り値が参照型で、その参照先が関数の引数のいずれかでないとすると、 その参照先は関数内で生成された値への参照となるが、これは dangling pointer になるのでコンパイルでエラーになる。

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str() // コンパイルエラー
}

この場合の修正方法は、戻り値を String型に変更し、呼び出し側に所有権を持たせるようにする。

つまるところ、Rustのライフタイムのシンタックスは、関数の引数のライフタイムと戻り値のライフタイムを紐付け、 コンパライが dangling pointer のチェックをするのに必要な情報を提供するためのもの、と言える。

Lifetime Elision

ライフタイムアノテーションの付与がコードから明かな場合、省略することができる。 (昔のバージョンのRustでは全て手動で書く必要があった。今後コンパイラの推論の精度が上がると、手動で付与する場面がさらに減るかもしれない)

コンパイラは、以下3つのルールを使って引数/戻り値のライフタイムを推測しようとする。 3つのルールのうち、最初の1つが関数の引数に適用されるもので、残りの2つは戻り値に適用されるものである。

  • ルール1: 全ての参照型の引数に別々のライフタイムパラメータを設定する
  • ルール2: 引数のライフタイムパラメータが1つだけ設定されている場合、戻り値の全てに同じライフタイムパラメータが設定される
  • ルール3: 引数が複数あるケースで、そのうちの1つ引数が &self または &mut self の場合(つまり、この関数が構造体のメソッドの場合)、 self のライフタイムが全ての戻り値のライフタイムとして設定される。

これらのルールを適用したうえでなお、引数/戻り値のライフタイムが定まらない場合、コンパイラエラーが発生する。 その場合、実装者がライフタイムアノーテーションを手動で付与する。

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

これにルール1と2を適用すると

fn first_word<'a>(s: &'a str) -> &'a str {

となり。ライフタイムが確定する。したがって実装者はアノテーションを付与する必要はない。

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

この場合、ルール1は適用できるが2と3は適用できない。

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

これ状態がルール適用後となり、ライフタイムが確定しないので実装者がアノテーションを付与することになる。

Lifetime Annotations in Method Definitions

ライフタイムがある構造体のメソッドの場合、 impl の後と構造体名の後にはライフタイムアノテーションが必要。

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

ルール1と3により、以下の announce_and_return_part メソッドの引数/戻り値のライフタイムは定まる。

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