ライフタイムの必要性
宣言された変数はスコープを抜けるとドロップされ、もう参照することはできません。そのため、ある変数A
を参照するための別の変数B
が存在している場合、A
が生存できるスコープ外でB
を参照しようとするとコンパイルエラーになります。
fn main() {
let r;
{
let x = 5;
r = &x; // エラー: `x`はこのスコープを抜けるとドロップされる
}
println!("r: {}", r); // ここで参照`r`を使うのはダングリングになる
}
このコンパイルエラーを検出するために、Rustには「ライフタイム」という仕組みが存在します。変数ごとのライフタイムを管理することで、ダングリングを防止します。
ダングリングとは、ポインタが不正なメモリ位置を参照してしまうことを指します。
関数とライフタイム
ダングリングを防止するためにライフタイムは存在するため、関数が戻り値を返すとき、その戻り値のライフタイムも正しく把握する必要があります。そうでないと、関数を抜けた後に戻り値を参照してダングリングが発生する恐れがあります。
例えば、String
への参照を返す関数があったとして、その戻り値は関数内で宣言された変数への参照だったとします。このような実装をしてしまうと、それはコンパイルエラーになります。
fn invalid_reference() -> &String {
let s = String::from("Hello");
&s // エラー: `s`は関数が終了するとドロップされる
}
fn main() {
let r = invalid_reference();
println!("{}", r);
}
これがエラーになるのは、参照先の変数は関数を抜けた段階でドロップされ、無くなってしまうからです。関数が終了して呼び出し元に処理が戻った時、戻り値が指し示すメモリにはもうデータは存在しません。すなわちダングリングが発生してしまうため、ライフタイムが関数に限られている変数は返せないのです。
明示的なライフタイムの指定
関数の戻り値のライフタイムは明示的に指定することが可能です。関数においてライフタイムを扱いたい場合(指定したい場合)は、<'a>
を用います。この'a
を、ジェネリックなライフタイムと呼びます。
関数にジェネリックなライフタイムを導入するメリットは何でしょうか。それは、戻り値のライフタイムを指定できることにあります。関数内で宣言した変数への参照の返却が失敗するのは前項の通りですが、たとえ引数を基に参照を返却しようとしてもエラーになるケースが存在します。
例として、二つの&str
を受け取り、長い方を返却する関数を考えます。ライフタイム注釈が無いとコンパイルエラーになります。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("long string is long");
let str2 = "short";
let result = longest(str1.as_str(), str2);
println!("The longest string is '{}'", result);
}
なぜかというと、戻り値のライフタイムを決定できないからです(x
かもしれないしy
かもしれない)。ダングリングを防ぐには戻り値のライフタイムを把握する必要がありますが、どれくらいのライフタイムを持つのかが判断できません。
ただし、判断できないと言っても全く手掛かりがないわけではなく、少なくとも引数x
かy
のライフタイムのどちらかではあるはずです。そこで、ジェネリックなライフタイムを使うことで、そのことをコンパイラに教えることができます。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("long string is long");
let str2 = "short";
let result = longest(str1.as_str(), str2);
println!("The longest string is '{}'", result);
}
引数は二種類なので、厳密にいえばx
とy
のライフタイムは異なります。そのため、'a
と'b
の両方書かなければならないように思えますが、上記のサンプルコードのように書くと、自動的にx
とy
のライフタイムのうち短い方が'a
に設定されます。そのため、どちらのライフタイムが短いかを気にすることなく、ただ短い方の参照が返ることを-> &'a str
で表現できるのです。
これは借用という機構をもつRustならではの仕組みです。正直なところ開発者としては難しいだけのように思えますが、コンパイラが丁寧に怒ってくれるので実際はそこまで悩まないで済むかもしれません。
ちなみに、引数が一つしかない場合など、コンパイラが自動でライフタイムを推定してくれて必ずしもライフタイム注釈を書かなくてもいいケースがあります(これまで書いてきたもののほとんどがそう)。