やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Rustのライフタイム1

 Rustにおける難関のひとつらしい。

成果物

参考

ライフタイムとは

ライフタイムとは、その参照が有効になるスコープのこと

ライフタイムの主目的

ライフタイムの主な目的は、ダングリング参照を回避すること

{
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r); // error[E0597]: `x` does not live long enough
}

 上記はrを参照したとき「xの寿命が足りない」とコンパイルエラーになる。rxを参照している。xは内側の{}終端で死んだ。死亡後に参照したため本エラーとなっている。

 つまりRustはダングリング参照するとコンパイルエラーになる。その実行を未然に防いでくれる。不正なメモリ領域にアクセスしてセキュリティホールになったり、システム破壊される心配がなくなる。メモリ安全性が保たれた。

 以下はコンパイル成功する。xが生きている間に参照しているため。

{
    let r;
    {
        let x = 5;
        r = &x;
        println!("r: {}", r); // OK
    }
}

借用精査機

 Rustコンパイラには借用精査機(借用チェッカー)がある。変数のスコープを比較して全ての借用が有効であるかを判断する。

{
    let r;                // ---------+-- 'a
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
    println!("r: {}", r); //          |
}                         // ---------+
  • 変数r'aという名前のライフタイムである
  • 変数x'bという名前のライフタイムである

 rのライフタイムは'aであり、println!よりも長いためダングリング参照にはならない。だが、変数rは変数xの参照であるため、xのライフタイム次第である。そしてxのライフタイム'bprintln!の前に死んでいる。よってrの内容であるxの参照は死んでいるためダングリング参照となり、コンパイルエラーとなる。

関数のジェネリックなライフタイム

 2つのうち長いほうの文字列を返す。

fn main() {
    let string1 = String::from("AA");
    let result;
    {
        let string2 = String::from("A");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("{}", result);
}

 以下の関数を定義するとコンパイルエラーになる。「戻り値にライフタイム指定子がない」という。

fn longest(x: &str, y: &str) -> &str { // error[E0106]: missing lifetime specifier
    if x.len() > y.len() { x }
    else { y }
}

ライフタイム注釈記法

 以下のようにすると解決する。

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

 ライフタイム注釈はいかなる参照の生存期間も変えない。ライフタイム注釈は、借用精査機では解明できない変数のライフタイムを明示するためにある。

 前項のコンパイルエラーは戻り値のライフタイムがxyの2パターンあり一意に特定できないせいで生じた。これをライフタイム注釈により指定してやることで戻り値のライフタイムが定まりコンパイルエラーを回避できる。

  • ライフタイム注釈<'a>ジェネリクス同様<>内に書く
  • ライフタイム注釈'aを引数と戻り値に書くことで戻り値のライフタイムを示す
    • 戻り値のライフタイムは、両引数の短命なほうと同じである
&i32        // 参照
&'a i32     // ライフタイムを明示した参照
&'a mut i32 // ライフタイムを明示した可変参照

 ライフタイム注釈はひとつの変数に対して書いても意味はない。複数の変数に対して注釈することでダングリング参照を回避することが目的。

関数シグニチャにおけるライフタイム注釈

 2つのうち長いほうの文字列を返す関数。

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

 以下はコンパイルできる。

fn main() {
    let string1 = String::from("AA");
    let result;
    {
        let string2 = String::from("A");
        result = longest(string1.as_str(), string2.as_str());
        println!("{}", result); // コンパイル成功。短いほうstring2のライフタイム範囲内であるため
    }
}

 以下はコンパイルできない。

fn main() {
    let string1 = String::from("AA");
    let result;
    {
        let string2 = String::from("A");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("{}", result); // コンパイルエラー。短いほうstring2のライフタイム範囲外であるため
}
error[E0597]: `string2` does not live long enough
  --> src/main.rs:15:5
   |
14 |         result = longest(string1.as_str(), string2.as_str());
   |                                            ------- borrow occurs here
15 |     }
   |     ^ `string2` dropped here while still borrowed
16 |     println!("The longest string is {}", result);
17 | }
   | - borrowed value needs to live until here

 文字列が長いほうはstring1である。そちらはライフタイムがprintln!より長いため成功してもいいはず。だが、longest関数のライフタイム注釈により、その戻り値は2引数のうち短いほうを指定したことになる。よってlongest関数の戻り値におけるライフタイムは、ライフタイムが長いstring1が返るにも関わらず、ライフタイムが短いstring2のライフタイムとなる。そしてstring2のライフタイム範囲外で戻り値を参照した結果、「string2の寿命が短い」というコンパイルエラーとなった。

ライフタイムの観点で思考する

 以下はyにライフタイム注釈が不要となる。yを返すことはないため。

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

 以下はコンパイルエラー。戻り値のライフタイムは引数のライフタイムのうちどれかに一致する必要がある。以下は引数のどこにもライフタイム注釈がないためダングリング参照エラーとなる。戻り値はlongest関数内で生成した変数であり、ライフタイムもlongenst関数内である。関数終了時に死ぬ変数の参照を返すことになり、ダングリング参照となる。

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str() // error[E0597]: `result` does not live long enough
}

 解決するには、所有権をムーブ(委譲)すればいい。

fn longest(x: &str, y: &str) -> String {
    let result = String::from("really long string");
    result
}

 これにてlongest関数内にあった変数resultの所有権は、戻り値を受け取った変数へとムーブする。その変数が有効なスコープが、resultの新たなライフタイムとなる。

疑問

 ドキュメントにはなかったが、以下ではなぜかコンパイルエラーにならない。

 どちらもStringでなくstr型にすることでエラーが回避されてしまう。謎。

疑問1

fn main() {
    let str1 = "AA".to_string();
    let result;
    {
        let str2 = "A";
        result = longest(str1.as_str(), str2);
    }
    println!("{}", result); // error[E0597]: `str2` does not live long enough になるはず
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x }
    else { y }
}

 let str2 = "A";let str2 = "AA".to_string();にすると想定通りエラーとなる。&str型にするとライフタイムが伸びるわけでもあるまいに、どういうことか?

疑問2

fn main() {
    let str1 = "AA".to_string();
    let result;
    {
        let str2 = "A";
        result = longest(str1.as_str(), str2);
    }
    println!("{}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    "A" // なぜか成功してしまう。「error[E0597]: `str2` does not live long enough」にならないのはおかしいのでは?
}

 ライフタイム注釈によってライフタイムが変わることがないなら、戻り値"A"の生存期間はlongest関数の終端で終わるはず。それをlongest関数の戻り値として返せばダングリング参照エラーのはず。戻り値は参照であるため、所有権のムーブも起こらないはず。なぜエラーにならない?

 「ライフタイム注釈によってライフタイムが変わることがない」という説明は本当に正しいのだろうか……。あるいは何かを知らないか勘違いしているか。謎。

所感

 まだライフタイムのさわりでしかないのに、疑問が解決できない。つまり理解できていないということ。でも、今は飛ばして読み進めるしかない。あーモヤモヤする。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで