コンテンツにスキップ

Rustのライフタイム

ライフタイム(lifetime)は、Rustを学び始めた人が最初に「壁」を感じやすいところです。でも安心してください。このページの目標は、ライフタイムが何なのか、なぜコンパイラがそれを求めてくるのかがわかるようになることだけです。細かいルールを全部覚える必要はありません。

借用のページで、参照(&)を使えば値を所有せずに使えることを学びました。ただし、参照が有効なのは、指している値がまだ存在している間だけです。Rustは、すでに片付けられた値を指す参照が残ってしまわないように気をつけています。このようなバグをダングリング参照(dangling reference)と呼び、C言語などではクラッシュの典型的な原因になっています。

ほとんどの場合、Rustはコードのスコープを見て自分でこれを判断してくれるので、あなたが意識する必要はありません(スコープのページで見たのがまさにこれです)。ただ、特に関数では、コンパイラが自力で判断できず、あなたにはっきり書いてほしいと頼んでくることがあります。そのときに書くのがライフタイム注釈です。

2つの文字列スライスのうち、長いほうを返す関数を見てみましょう。

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

これはコンパイルできません。次のようなエラーが出ます。

error[E0106]: missing lifetime specifier

「ライフタイム指定子がありません」という意味です。問題は、返される参照が a から借りたものなのか b から借りたものなのか、関数を読んだだけではRustにはわからないことです。そのため、受け取った参照を使う時点でそれがまだ有効だと保証できないのです。

解決するには、ライフタイムに名前を付け(慣習として 'a のような小文字1文字を使います)、戻り値のライフタイムが両方の引数と結びついていることをRustに伝えます。

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("長いほう: {result}");
}

実行結果:

長いほう: hello
▶ 実際に試してみよう

比べる文字列を英語のままにしているのは、この関数が .len()(バイト数)で長さを比べているからです。日本語の文字はUTF-8で1文字3バイトになるので、見た目の文字数と結果がずれることがあります。

'a は値ではなく「ラベル」だと考えてください。実際の長さ(期間)を決めるものではありません。「これらの参照は、少なくとも同じ範囲のコードの間は有効でなければならない」とRustに伝えているだけです。あとはコンパイラが、関数を呼び出しているすべての場所で本当にそうなっているかをチェックしてくれます。

この注釈がないと、longest が返す参照が、元になった文字列のどちらかより長生きしてしまうかどうかを、コンパイラは本当に判断できません。ライフタイム注釈があるおかげで、返ってくる参照がいつでも安全に使えることを、コンパイル時に、しかも実行時のコストなしで証明できるのです。

次にライフタイムに出会うのは構造体です。これまでは、構造体のフィールドはどれも String のように自分のデータを所有していました。フィールドを参照にすることもできますが、その場合、構造体が有効なのは指しているデータが存在する間だけになります。Rust はそれをライフタイムで書くように求めます。

struct Highlight<'a> {
text: &'a str, // どこか別の場所にある String から借りている
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("ハイライト: {}", first.text);
println!("本全体の長さは{}バイト", book.len());
}

実行結果:

ハイライト: Call me Ishmael
本全体の長さは34バイト
▶ 実際に試してみよう

Highlight<'a> は「ライフタイム 'a の間、何かを借りている Highlight」と読みます。&book[..15] の部分はスライスで、book の先頭15バイトをコピーせずに借ります。first は借りているだけなので、そのあとも book を使えます。本文を英語のままにしているのは、バイト単位で切り出しているからです。日本語の文字は1文字3バイトなので、日本語にすると切り出す位置がずれてしまいます。

ライフタイムがあるおかげで、Rust は次のような間違いを見つけられます。構造体がまだ指しているのに、String が破棄されてしまうケースです。

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // ここで book が破棄される……
println!("{}", highlight.text); // ……なのに highlight はまだ book を指している
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // ここで book が破棄される……
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ……なのに highlight はまだ book を指している
| -------------- borrow later used here

このエラーは「book の生存期間が足りません」という意味です。book は借用されたまま } で破棄され、そのあとで借用が使われている、と教えてくれています。

実践的なコツをひとつ。構造体のライフタイムが邪魔になってきたら、&str の代わりに所有する String を持たせましょう。テキストはコピーされますが、構造体はほかの何にも依存しなくなり、ライフタイムも消えます。経験豊富な Rust プログラマーの多くも、まずはそう書いて、理由があるときだけ参照に切り替えています。

実際には、このページから受ける印象よりずっと少ない頻度でしか 'a を書きません。日常的な場面のほとんどではコンパイラがライフタイムを推論してくれるので、明示的なライフタイムをまったく書かないRustのコードもたくさんあります。主に出会うのは、関数が参照を返し、それがどこから来たのかをコンパイラが自力で判断できないときです。そんなときは「これはバグではなく、コンパイラが自分では証明できないことを確認してほしいと頼んでいるだけだ」と思い出してください。

2つの文字列スライスを受け取り、短いほうを返す関数 shortest を書いてください。同じ長さなら1つ目を返します。"elephant" と "cat" で呼び出してみましょう。

ヒント:上の longest と同じ形です。関数名のあとに <'a> を、両方の引数と戻り値の型に 'a を付けます。

解答を見る
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("elephant");
let small = String::from("cat");
println!("短いほう: {}", shortest(&big, &small));
println!("同じ長さなら1つ目: {}", shortest("dog", "cow"));
}

実行結果:

短いほう: cat
同じ長さなら1つ目: dog
▶ 実際に試してみよう

返されるスライスは a から来るかもしれないし、b から来るかもしれません。そのため Rust は 'a によって、結果が有効なのは両方の入力が有効な間だけだと知る必要があります。< ではなく <= を使っているので、同じ長さのときは a が選ばれます。ここでも .len()(バイト数)で比べるので、単語は英語のままにしています。

text と author という2つの &str フィールドを持つ構造体 Quote を作ってください。text は String "Fearless concurrency is a goal of Rust" の先頭20バイトを借用し、author はリテラル "The Rust Book" にします。Fearless concurrency (The Rust Book) と表示してください。

ヒント:上の Highlight と同じように、構造体にはライフタイムが必要です。struct Quote<'a> と書き、両方のフィールドを &'a str にします。&speech[..20] で先頭20バイトのスライスが取れます。

解答を見る
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

実行結果:

Fearless concurrency (The Rust Book)
▶ 実際に試してみよう

quote は speech から借りているので、speech より長生きすることはできません。リテラルの "The Rust Book" はプログラムに埋め込まれていて、実行中ずっと存在するので、'a の間は余裕で有効です。上のエラーの例のように、speech を内側の { } ブロックの中で作り、ブロックが終わったあとで引用を表示してみてください。コンパイラが止めてくれるのがわかります。

  • ライフタイムは、参照どうしの関係をコンパイラに伝え、ダングリング参照を防げるようにするものです。
  • 書く必要があるのは、コンパイラが自力で推論できないときだけです。いちばん多いのは、参照を返す関数です。
  • 'a は期間を決めるものではなく、異なる参照のライフタイムを結びつけて、コンパイラがチェックできるようにするものです。
  • 参照を持つ構造体にもライフタイムが必要で(struct Highlight<'a>)、借りているデータより長生きできません。所有する String を持たせれば、この制約はなくなります。
  • これは完全にコンパイル時のチェックで、実行時のコストはかかりません。

理解度チェック

1. ライフタイムはどんな問題を解決しますか?

2. fn longest(a: &str, b: &str) -> &str がライフタイム注釈なしでコンパイルできなかったのはなぜですか?

3. ライフタイム注釈を付けると、実行時に何かコストがかかりますか?

4. このページによると、明示的なライフタイムを書く必要があるのは主にどんなときですか?

スコア: 0 / 4