ライフタイムについて
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}");
}人間であれば result が println! まで生きてることが見て分かるが、
コンパイラはあくまでライフタイムパラメータで示した取り決めに反してないかどうかしか分からない。
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
}
}