Rustにおける難関のひとつらしい。
成果物
参考
ライフタイムとは
ライフタイムとは、その参照が有効になるスコープのこと
ライフタイムの主目的
ライフタイムの主な目的は、ダングリング参照を回避すること
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r); // error[E0597]: `x` does not live long enough
}
上記はrを参照したとき「xの寿命が足りない」とコンパイルエラーになる。rはxを参照している。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のライフタイム'bはprintln!の前に死んでいる。よって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 } }
ライフタイム注釈はいかなる参照の生存期間も変えない。ライフタイム注釈は、借用精査機では解明できない変数のライフタイムを明示するためにある。
前項のコンパイルエラーは戻り値のライフタイムがxとyの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関数の戻り値として返せばダングリング参照エラーのはず。戻り値は参照であるため、所有権のムーブも起こらないはず。なぜエラーにならない?
「ライフタイム注釈によってライフタイムが変わることがない」という説明は本当に正しいのだろうか……。あるいは何かを知らないか勘違いしているか。謎。
所感
まだライフタイムのさわりでしかないのに、疑問が解決できない。つまり理解できていないということ。でも、今は飛ばして読み進めるしかない。あーモヤモヤする。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13
- bash 4.4.12(1)-release
- rustc 1.34.2 (6c2484dc3 2019-05-13)
- cargo 1.34.0 (6789d8a0a 2019-04-01)
$ uname -a Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux
前回まで
- Rustを学んでみたい(プログラミング言語)
- Rustの環境構築
- RustでHelloWorld
- Rustの和訳ドキュメント
- Cargoでプロジェクト作成・ビルド・実行
- クレートとは?
- Rustで関数を使ってみる
- Rustでモジュールを使ってみる
- Rustで乱数を生成する(rand)
- Rustで標準入力する(std::io::stdin().read_line())
- RustでMatch判定する(match)
- Rustでprintとread_lineを1行にする方法
- Rustで数当てゲーム
- クレート名にドット.が使えない
- Rustの変数と可変性(let, mut) error[E0384]: cannot assign twice to immutable variable
x - Rustのimmutable束縛とconst定数の違い
- RustのREPL、evcxrのインストールに失敗した
- Rustでコンパイルするときの変数未使用warningを消す
- Rustの変数(再代入、再宣言(シャドーイング))
- Rustのシャドーイングについて
- イミュータブルについて(副作用)
- Rustの定数(const)
- Rustのデータ型(数値)
- Rustのデータ型(論理)
- Rustのデータ型(文字)
- Rustのデータ型(タプル)
- Rustのデータ型(配列)
- Rustの関数
- Rustのif式
- Rustのくりかえし文(loop)
- Rustのくりかえし文(while)
- Rustのくりかえし文(for)
- Rustの所有権(ムーブ)
- Rustの所有権(関数)
- Rustの所有権(スライス)
- Rustの構造体(定義とインスタンス化)
- Rustの構造体(プログラム例)
- Rustの構造体(メソッド)
- Rustの列挙型(enum)
- Rustの列挙型(enum)
- Rustの列挙型(enum)
- Rustのmatch(制御フロー演算子)
- RustでNULLを扱う(Option, Some, None)
- NULL参照は10億ドルの失敗だった
- Rustの列挙型に独自表示を実装する(E0277 対策 std::fmt::Display 実装)
- RustのIfLet(matchの糖衣構文)
- Rustのプロジェクト構造
- Rustのcargoでライブラリ&テスト(単体、結合)
- Rustのモジュール(mod)
- Rustのモジュール(pub)
- Rustのmod参照方法(
mod 子モジュール名;,use 要素名;,extern crate クレート名;,super) - Rustのインポートまとめ(Rust2018)
- RustのコレクションVec型
- RustのコレクションString型
- RustのコレクションHashMap型
- Rustのコレクション(練習問題)
- Rustのエラー処理
- Rustのジェネリクス
- Rustのトレイト