Rustのライフタイムについて

rustにはライフタイムという概念があります。みんなひっかかると思いますが、私も引っかかりました。。。

自分用のメモなのでわかりづらくても悪しからず。

ライフタイム

ライフタイムとは、参照が有効なスコープのことです。 rustではスコープを抜けるとメモリが開放されます。例えば、

変数xyのライフタイムは

fn main() {
    let x = String::from("HogeHoge"); // x ここから
    {
        let y = String::from("fugafuga"); // y ここから
    } // y ここまで
    println!("{}",y);
}// x ここまで

こうなるのは他の言語でもよくあることだと思います。 そこで、rustでは開放されたメモリポインタ(ダングリングポインタ)にアクセスしたり参照できないようになっています。 rustのコンパイラには借用チェッカーが存在しています。 これが、ダングリングポインタを参照しないようにしています。

基本的に参照全てにライフタイムを考える必要があります 例外を最後の方に示します。

借用チェッカー

長い文字列を変換する関数longestに関して、

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

fn main() {
    let x = String::from("BoKuToTsuZenU");
    let y = String::from("Tohoku Univ");

    let longer = longest(x, y);
}

これはエラーになります。

   Compiling rust_test v0.1.0 (/Users/hikarukondo/Documents/rust/rust_test)
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:39
  |
1 | fn longest(x: &String, y: &String) -> &String {
  |               -------     -------     ^ 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`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(x: &'a String, y: &'a String) -> &'a String {
  |           ^^^^    ^^^^^^^^^^     ^^^^^^^^^^     ^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `rust_test`.

To learn more, run the command again with --verbose.

これでエラーになる理由は、戻り値の参照がx,yになるか、コンパイラが判別できないため、戻り値がどちらのライフタイムになるかわからないからです。

ライフタイム引数の記法

この問題を解決するため、参照間の関係を定義するライフタイム引数を用います。 rustのライフタイム引数の定義の仕方は'aみたいな感じで書きます。

ライフタイム引数を持つ関数

同じライフタイム引数では同じライフタイムを持つものとしてコンパイルされます。 longest関数の入力は同じライフタイムを持つものとして同じライフタイム引数を使用します。

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

fn main() {
    let x = String::from("BoKuToTsuZenU");
    let y = String::from("Tohoku Univ");

    let longer = longest(x, y);
}

こんな感じでライフタイム引数で修飾するとコンパイルできます。 ジェネリックなライフタイム'aは、xyのライフタイムのうち、小さい方に等しい具体的なライフタイムになります。

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

fn main() {
    let x = String::from("BoKuToTsuZenU");
    let longer;
    {
        let y = String::from("Tohoku Univ");
        longer = longest(x, y);
    }
    println!("{}", longer);
}

このコードはエラーになります。 なぜなら、戻り値のライフタイムは入力のライフタイムの短い方になるからです。 つまり、戻りのライフタイムはyのライフタイムと同じです。 そのため、エラーが起こります。

以降は追記します。

構造体のライフタイム

まずこのコードを見てください。

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

これはエラーを吐きます。構造体の要素に参照を持つことも可能ですが、ライフタイムの注釈が必要です。

struct User<'a> {
    username: &'a str,
    email: &'a str,
    sign_in_conunt: u64,
    active:bool
}

で宣言することが可能です。

ライフタイム引数を持つ構造体のメソッド

impl<'a> User<'a> {
    fn somefunc(&self, T){ /* do something */}
}

のように宣言すればできます。

ライフタイムの省略

rustのライフタイムは元々全ての関数で書く必要があったらしいです。 ですが、数多くのコードを解析した結果一意にライフタイムを推定できる場合にはライフタイムの省略することが可能になりました。

ライフタイムの省略には規則があります(参照)。 コンパイラが以下の規則を用いてライフタイムを推定できない参照が存在する場合エラーになります。

  1. 入力に対して:参照である各引数は、独自のライフタイム引数を得る。 fn somefunc<'a>(x: &'a str)や、fn somefunc<'a, 'b>(x: &'a str, y; &'b str)など。 それぞれの引数に対して別々のライフタイムを与えるということだと思います。

  2. 1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入される fn somefunc<'a>(x: &'a str) -> &'a strみたいな場合。

  3. 複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&self&mut selfだったら、 selfのライフタイムが全出力ライフタイム引数に代入される

例題

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

上記のコードは動作します。 参照を引数にとり、参照を戻り値にしています。なぜ、コンパイルできるのでしょうか?

私たちはコンパイラ様のお気持ちを考える必要があります。 まず最初の規則参照である各引数は、独自のライフタイム引数を得るを適応します。 つまり、引数は全部独自のライフタイムをうけるとします。 今回は一つしか引数がないのでこの関数のシグニチャは以下のようになります。 fn first_word<'a>(s: &'a str) -> &str{ /* do something */} 次に、二つ目の引数に対してです。 この関数の引数は一つなので、適応できて、fn fiet_word<'a>(s: &'a str) -> &'a str {/* do something */} これにてめでたく、入出力に関してライフタイムは全て推測することができました。

次の例題です。

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

これに対しても同じように考えましょう。 まず第一の規則を適応してfn longest<'a,'b>(x: &'a str, y: &'a str) -> &str 次にこの関数の引数は二つのため、出力のライフタイムを推測することはできません。 さらに、第三の規則は全く関係ないので、出力の参照のライフタイムを推定することができません。 そのためこのシグニチャではコンパイラ様はライフタイムを推定できないのでエラーです。

静的ライフタイム

'static

で宣言できる。 この宣言の参照はプログラムの全期間を表している。

まとめ

これからもrustのコンパイラとの戦いは続きそうです。 困ったらここを見よう