ライフタイムで参照を検証する
ライフタイムは、私たちがすでに使ってきたもう 1 種類のジェネリクスです。 型が望んだ振る舞いを持っていることを保証するのではなく、ライフタイムは 必要なあいだ参照が有効であることを保証します。
第 4 章の 「参照と借用」 節で 触れなかった詳細の 1 つは、Rust におけるすべての参照にはライフタイムがあり、 その参照が有効であるスコープを表すということです。ほとんどの場合、 ライフタイムは暗黙的で、型推論と同じように推論されます。複数の型が あり得る場合にだけ型注釈が必要になるのと同様に、参照のライフタイムが いくつかの異なる形で関係し得る場合には、ライフタイム注釈を付けなければ なりません。Rust は、実行時に実際に使われる参照が確実に有効になるよう、 ジェネリックなライフタイムパラメータを使ってその関係を注釈することを 要求します。
ライフタイムに注釈を付けるという考え方は、ほかの多くのプログラミング言語 にはまったくないものなので、見慣れないと感じるでしょう。この章では ライフタイムのすべてを扱うわけではありませんが、この概念に慣れられるよう、 ライフタイム構文に出会う典型的な場面について説明します。
ダングリング参照
ライフタイムの主な目的は、ダングリング参照を防ぐことです。もしそれが 存在できてしまうと、プログラムは本来参照すべきデータとは別のデータを 参照してしまうことになります。外側のスコープと内側のスコープを持つ、 リスト 10-16 のプログラムを考えてみましょう。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
注: リスト 10-16、10-17、10-23 の例では、変数に初期値を与えずに 宣言しているため、変数名は外側のスコープに存在します。一見すると、 これは Rust に null 値がないことと矛盾しているように見えるかもしれません。 しかし、値を与える前に変数を使おうとするとコンパイル時エラーになり、 そのことから実際に Rust は null 値を許していないことが分かります。
外側のスコープでは、初期値を持たない r という変数を宣言し、内側の
スコープでは、初期値 5 を持つ x という変数を宣言します。内側の
スコープの中で、r の値を x への参照にしようとします。その後、
内側のスコープが終わり、r に入っている値を出力しようとします。この
コードはコンパイルできません。r が参照している値が、使おうとする前に
スコープを抜けてしまっているからです。以下がエラーメッセージです。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
エラーメッセージは、変数 x が「十分長く生存しない」と述べています。
理由は、7 行目で内側のスコープが終わるときに x がスコープ外になるから
です。しかし r はまだ外側のスコープで有効です。そのスコープのほうが
大きいため、「より長く生きる」と言います。もし Rust がこのコードを
動作させることを許したなら、r は x がスコープを抜けたときに解放
されたメモリを参照することになり、r で何をしようとしても正しく動作
しなくなります。では、Rust はどのようにしてこのコードが不正だと判断
するのでしょうか。借用チェッカーを使います。
借用チェッカー
Rust コンパイラには、すべての借用が有効かどうかを判断するためにスコープ を比較する 借用チェッカー があります。リスト 10-17 はリスト 10-16 と 同じコードですが、変数のライフタイムを示す注釈が付いています。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
ここでは、r のライフタイムに 'a、x のライフタイムに 'b という
注釈を付けています。ご覧のとおり、内側の 'b ブロックは、外側の 'a
ライフタイムブロックよりずっと小さくなっています。コンパイル時に、
Rust は 2 つのライフタイムの大きさを比較し、r は 'a のライフタイム
を持つ一方で、'b のライフタイムを持つメモリを参照していることを
確認します。プログラムは却下されます。なぜなら 'b は 'a より短い
からです。つまり、参照先は参照そのものと同じだけ長く生きないのです。
リスト 10-18 では、ダングリング参照が生じないようにコードを修正しており、 エラーなくコンパイルできます。
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
ここで、x はライフタイム 'b を持ち、この場合 'a より長くなって
います。つまり r は x を参照できます。なぜなら、x が有効である
あいだ、r に入っている参照も常に有効だと Rust が分かっているからです。
これで、参照のライフタイムがどこにあり、Rust がライフタイムをどのように 解析して参照が常に有効であることを保証しているかが分かったので、関数の 引数と戻り値におけるジェネリックなライフタイムを見ていきましょう。
関数におけるジェネリックなライフタイム
2 つの文字列スライスのうち長いほうを返す関数を書いてみましょう。この
関数は 2 つの文字列スライスを受け取り、1 つの文字列スライスを返します。
longest 関数を実装したあと、リスト 10-19 のコードは
The longest string is abcd と出力するはずです。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
この関数には文字列そのものではなく、参照である文字列スライスを受け取ら
せたいことに注意してください。longest 関数に引数の所有権を奪わせたく
ないからです。リスト 10-19 で使っている引数が望ましいものである理由に
ついては、第 4 章の 「引数としての文字列スライス」
を参照してください。
リスト 10-20 のように longest 関数を実装しようとすると、コンパイル
できません。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
代わりに、ライフタイムに関する以下のエラーが出ます。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ヘルプテキストを見ると、戻り値の型にはジェネリックなライフタイム
パラメータが必要だと分かります。Rust には、返される参照が x を指す
のか y を指すのか判断できないからです。実際、私たちにも分かりません。
この関数本体の if ブロックは x への参照を返し、else ブロックは
y への参照を返すからです。
この関数を定義している時点では、この関数に渡される具体的な値がわからないため、if のケースと else のケースのどちらが実行されるかはわかりません。また、渡される参照の具体的なライフタイムもわからないため、リスト10-17と10-18で行ったようにスコープを見て、返す参照が常に有効かどうかを判断することもできません。借用チェッカーにもこれを判断することはできません。なぜなら、x と y のライフタイムが戻り値のライフタイムとどのような関係にあるのかを知らないからです。このエラーを修正するために、参照同士の関係を定義するジェネリックなライフタイムパラメータを追加し、借用チェッカーが解析を実行できるようにします。
ライフタイム注釈の構文
ライフタイム注釈は、どの参照がどれだけ長く生きるかを変更するものではありません。そうではなく、ライフタイムに影響を与えることなく、複数の参照のライフタイム同士の関係を記述するものです。関数がシグネチャでジェネリックな型パラメータを指定することで任意の型を受け取れるのと同じように、関数はジェネリックなライフタイムパラメータを指定することで、任意のライフタイムを持つ参照を受け取れます。
ライフタイム注釈の構文は少し独特です。ライフタイムパラメータの名前はアポストロフィ (') で始めなければならず、通常はすべて小文字で、ジェネリック型のように非常に短い名前になります。ほとんどの人は、最初のライフタイム注釈に 'a という名前を使います。ライフタイムパラメータの注釈は参照の & の後ろに置き、注釈と参照先の型は空白で区切ります。
以下にいくつか例を示します。ライフタイムパラメータのない i32 への参照、'a というライフタイムパラメータを持つ i32 への参照、そして同じくライフタイム 'a を持つ i32 への可変参照です。
&i32 // 参照
&'a i32 // 明示的なライフタイムを持つ参照
&'a mut i32 // 明示的なライフタイムを持つ可変参照
ライフタイム注釈は1つだけではあまり意味を持ちません。というのも、注釈は複数の参照のジェネリックなライフタイムパラメータ同士がどのような関係にあるのかを Rust に伝えるためのものだからです。longest 関数の文脈で、ライフタイム注釈同士がどのように関係するのかを見ていきましょう。
関数シグネチャ内で
関数シグネチャでライフタイム注釈を使うには、ジェネリックな型パラメータのときと同じように、関数名とパラメータリストの間の山かっこの中でジェネリックなライフタイムパラメータを宣言する必要があります。
このシグネチャでは、次の制約を表したいと考えています。返される参照は、2つのパラメータの両方が有効である限り有効である、ということです。これが、パラメータのライフタイムと戻り値のライフタイムの関係です。ライフタイムに 'a という名前を付け、それを各参照に追加します。リスト10-21に示すとおりです。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
このコードはコンパイルでき、リスト10-19の main 関数と一緒に使うと、期待どおりの結果を生成するはずです。
この関数シグネチャは Rust に対して、あるライフタイム 'a について、この関数が2つのパラメータを受け取り、そのどちらも少なくともライフタイム 'a の間は有効な文字列スライスであることを伝えています。また、この関数シグネチャは、関数から返される文字列スライスも少なくともライフタイム 'a の間は有効であることを Rust に伝えています。実際には、これは longest 関数が返す参照のライフタイムが、関数引数が参照している値のライフタイムのうち短いほうと同じであることを意味します。これらの関係こそが、このコードを解析するときに Rust に使ってほしいものです。
覚えておいてください。この関数シグネチャでライフタイムパラメータを指定しても、渡される値や返される値のライフタイム自体を変更しているわけではありません。そうではなく、借用チェッカーはこれらの制約に従わない値を拒否すべきだと指定しているのです。longest 関数は、x と y が正確にどれだけ生きるかを知る必要はなく、このシグネチャを満たす何らかのスコープを 'a に当てはめられることだけが必要です。
関数でライフタイムを注釈する場合、注釈は関数本体ではなく関数シグネチャに記述します。ライフタイム注釈は、シグネチャ内の型とよく似た形で、関数の契約の一部になります。関数シグネチャにライフタイムの契約を含めることで、Rust コンパイラが行う解析はより単純になります。関数の注釈のしかたや呼び出しかたに問題がある場合、コンパイラエラーはコードのどの部分とどの制約に問題があるのかを、より正確に指し示せます。逆に、Rust コンパイラがライフタイム同士の関係について私たちの意図をもっと推論していたなら、コンパイラは問題の原因から何段階も離れたコードの使用箇所しか指し示せないかもしれません。
longest に具体的な参照を渡すとき、'a に代入される具体的なライフタイムは、x のスコープと y のスコープが重なっている部分になります。言い換えると、ジェネリックなライフタイム 'a には、x と y のライフタイムのうち短いほうと等しい具体的なライフタイムが入ります。返される参照にも同じライフタイムパラメータ 'a を注釈しているため、返される参照も x と y のライフタイムのうち短いほうの長さだけ有効になります。
ライフタイム注釈が longest 関数をどのように制限するのかを、具体的なライフタイムが異なる参照を渡して見てみましょう。リスト10-22はわかりやすい例です。
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
この例では、string1 は外側のスコープの終わりまで有効で、string2 は内側のスコープの終わりまで有効で、result は内側のスコープの終わりまで有効な何かを参照しています。このコードを実行すると、借用チェッカーがこれを許可することがわかります。コンパイルされ、The longest string is long string is long と出力されます。
次に、result 内の参照のライフタイムが2つの引数のうち短いほうのライフタイムでなければならないことを示す例を試してみましょう。result 変数の宣言を内側のスコープの外に移動しますが、result 変数への値の代入は string2 があるスコープの内側に残します。次に、result を使う println! を内側のスコープの外、つまりそのスコープが終わった後に移動します。リスト10-23のコードはコンパイルされません。
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}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
このコードをコンパイルしようとすると、次のエラーが表示されます。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
このエラーは、result が println! 文に対して有効であるためには、
string2 が外側のスコープの終わりまで有効である必要があることを示して
います。Rust がこれを知っているのは、同じライフタイムパラメータ 'a を
使って関数パラメータと戻り値のライフタイムを注釈したからです。
人間である私たちは、このコードを見て、string1 が string2 より長く、
したがって result には string1 への参照が入ることが分かります。
string1 はまだスコープを抜けていないため、string1 への参照は
println! 文でも依然として有効です。しかし、この場合にその参照が有効で
あることをコンパイラは見抜けません。私たちは、longest 関数によって返される
参照のライフタイムが、渡された参照のライフタイムのうち短い方と同じであると
Rust に伝えています。そのため、borrow checker はリスト 10-23 のコードを、
無効な参照を持つ可能性があるものとして許可しません。
longest 関数に渡す参照の値やライフタイム、および返された参照の使い方を
変えた、さらに多くの実験を設計してみてください。コンパイルする前に、その
実験が borrow checker を通過するかどうかについて仮説を立ててみましょう。
その後、実際に確かめて、自分の予想が正しかったか確認してください。
関係
どのようにライフタイムパラメータを指定する必要があるかは、関数が何をして
いるかによって異なります。たとえば、longest 関数の実装を、常に最長の
文字列スライスではなく最初のパラメータを返すように変更したとすると、y
パラメータにライフタイムを指定する必要はありません。次のコードはコンパイル
されます。
fn main() {
let string1 = String::from("abcd");
let string2 = "efghijklmnopqrstuvwxyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
x パラメータと戻り値の型にはライフタイムパラメータ 'a を指定しましたが、
y パラメータには指定していません。これは、y のライフタイムが x の
ライフタイムや戻り値と何の関係もないからです。
関数から参照を返す場合、戻り値の型のライフタイムパラメータは、いずれかの
パラメータのライフタイムパラメータと一致している必要があります。返される
参照がパラメータのいずれかを指していない場合、それはこの関数内で作成された
値を指していなければなりません。しかし、この場合、それはダングリング参照に
なってしまいます。なぜなら、その値は関数の終わりでスコープを抜けるからです。
コンパイルできない longest 関数の次の実装を考えてみましょう。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
ここでは、戻り値の型にライフタイムパラメータ 'a を指定しているにも
かかわらず、この実装はコンパイルに失敗します。というのも、戻り値の
ライフタイムがパラメータのライフタイムとまったく関係していないからです。
以下は実際に得られるエラーメッセージです。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
問題は、result が longest 関数の終わりでスコープを抜け、クリーンアップ
されることです。さらに、私たちはその関数から result への参照を返そうと
しています。このダングリング参照を変えることができるようなライフタイム
パラメータの指定方法は存在せず、Rust はダングリング参照を作ることを許し
ません。この場合の最善の修正は、参照ではなく所有権を持つデータ型を返す
ことです。そうすれば、その値のクリーンアップは呼び出し元の関数の責任に
なります。
最終的に、ライフタイム構文とは、関数のさまざまなパラメータや戻り値の ライフタイムを結び付けるためのものです。ひとたびそれらが結び付けられれば、 Rust はメモリ安全な操作を許可し、ダングリングポインタを作成したり、その ほかの形でメモリ安全性を損なったりする操作を拒否するための十分な情報を 得られます。
構造体定義内
ここまでに定義してきた構造体は、すべて所有権を持つ型を保持していました。
参照を保持する構造体を定義することもできますが、その場合は構造体定義内の
すべての参照にライフタイム注釈を追加する必要があります。リスト 10-24 には、
文字列スライスを保持する ImportantExcerpt という名前の構造体があります。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
この構造体には、文字列スライスを保持する part という単一のフィールドが
あります。文字列スライスは参照です。ジェネリックなデータ型と同様に、構造体名の
後ろの山かっこ内にジェネリックなライフタイムパラメータ名を宣言します。これに
より、構造体定義の本体でそのライフタイムパラメータを使用できます。この注釈は、
ImportantExcerpt のインスタンスが、その part フィールドに保持している
参照より長く生存できないことを意味します。
ここでの main 関数は、変数 novel が所有する String の最初の文への参照を
保持する ImportantExcerpt 構造体のインスタンスを作成します。novel の
データは ImportantExcerpt インスタンスが作成される前から存在しています。
さらに、novel は ImportantExcerpt がスコープを抜けた後までスコープを
抜けないため、ImportantExcerpt インスタンス内の参照は有効です。
ライフタイム省略
すべての参照にはライフタイムがあり、参照を使う関数や構造体にはライフタイム パラメータを指定する必要があることを学びました。しかし、リスト 4-9 で定義した 関数には、ライフタイム注釈がなくてもコンパイルできたものがありました。これを リスト 10-25 に再掲します。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
この関数がライフタイム注釈なしでコンパイルできる理由は、歴史的なものです。 Rust の初期バージョン(1.0 より前)では、このコードはコンパイルできません でした。というのも、すべての参照に明示的なライフタイムが必要だったからです。 当時、この関数シグネチャは次のように書かれていたはずです。
fn first_word<'a>(s: &'a str) -> &'a str {
大量のRustコードを書いたあと、Rustチームは、Rustプログラマが特定の状況で同じライフタイム注釈を何度も繰り返し書いていることを見いだしました。こうした状況は予測可能で、いくつかの決定的なパターンに従っていました。開発者たちはこれらのパターンをコンパイラのコードに組み込み、その結果、借用チェッカーはこうした状況ではライフタイムを推論できるようになり、明示的な注釈を必要としなくなりました。
このRustの歴史の一片が重要なのは、今後さらに多くの決定的なパターンが見つかり、コンパイラに追加される可能性があるからです。将来的には、必要となるライフタイム注釈はさらに少なくなるかもしれません。
Rustの参照解析に組み込まれているパターンは、ライフタイム省略規則 と呼ばれます。これらはプログラマが従うための規則ではありません。コンパイラが考慮する特定のケースの集合であり、あなたのコードがそれらのケースに当てはまるなら、ライフタイムを明示的に書く必要はありません。
省略規則は完全な推論を提供するわけではありません。Rustがこれらの規則を適用したあとでも、参照がどのライフタイムを持つかについて曖昧さが残る場合、コンパイラは残った参照のライフタイムがどうあるべきかを推測しません。推測する代わりに、コンパイラはエラーを出し、あなたはライフタイム注釈を追加することでそれを解決できます。
関数やメソッドのパラメータ上のライフタイムは 入力ライフタイム と呼ばれ、戻り値上のライフタイムは 出力ライフタイム と呼ばれます。
コンパイラは、明示的な注釈がないときに参照のライフタイムを導き出すために3つの規則を使います。最初の規則は入力ライフタイムに適用され、2番目と3番目の規則は出力ライフタイムに適用されます。コンパイラが3つの規則の終わりまで到達しても、なおライフタイムを導き出せない参照がある場合、コンパイラはエラーで停止します。これらの規則は fn 定義だけでなく impl ブロックにも適用されます。
最初の規則は、コンパイラが参照である各パラメータにライフタイムパラメータを割り当てる、というものです。言い換えると、1つのパラメータを持つ関数は1つのライフタイムパラメータを受け取ります: fn foo<'a>(x: &'a i32)。2つのパラメータを持つ関数は2つの別個のライフタイムパラメータを受け取ります: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。以降も同様です。
2番目の規則は、入力ライフタイムパラメータがちょうど1つである場合、そのライフタイムがすべての出力ライフタイムパラメータに割り当てられる、というものです: fn foo<'a>(x: &'a i32) -> &'a i32。
3番目の規則は、入力ライフタイムパラメータが複数あるが、そのうちの1つがメソッドであるため &self または &mut self である場合、self のライフタイムがすべての出力ライフタイムパラメータに割り当てられる、というものです。この3番目の規則により、必要な記号が少なくなるため、メソッドははるかに読み書きしやすくなります。
では、私たちがコンパイラだと仮定してみましょう。これらの規則を適用して、リスト10-25の first_word 関数のシグネチャにある参照のライフタイムを導き出します。シグネチャは、最初は参照に関連付けられたライフタイムがない状態で始まります。
fn first_word(s: &str) -> &str {
次に、コンパイラは最初の規則を適用します。この規則では、各パラメータがそれぞれ独自のライフタイムを受け取るとされています。いつものようにこれを 'a と呼ぶことにすると、シグネチャは次のようになります。
fn first_word<'a>(s: &'a str) -> &str {
入力ライフタイムがちょうど1つあるため、2番目の規則が適用されます。2番目の規則では、1つの入力パラメータのライフタイムが出力ライフタイムに割り当てられると定められているので、シグネチャは次のようになります。
fn first_word<'a>(s: &'a str) -> &'a str {
これで、この関数シグネチャ内のすべての参照がライフタイムを持つことになり、コンパイラは、この関数シグネチャでプログラマがライフタイムを注釈しなくても解析を続行できます。
次は別の例を見てみましょう。今度は、リスト10-20で最初に扱い始めたときにはライフタイムパラメータを持っていなかった longest 関数を使います。
fn longest(x: &str, y: &str) -> &str {
最初の規則を適用してみましょう。各パラメータがそれぞれ独自のライフタイムを受け取ります。今回は1つではなく2つのパラメータがあるので、2つのライフタイムがあります。
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
2つ以上の入力ライフタイムがあるため、2番目の規則は適用されないことがわかります。3番目の規則も適用されません。longest はメソッドではなく関数なので、どのパラメータも self ではないからです。3つの規則をすべて当てはめてみても、戻り値の型のライフタイムが何かはまだわかりません。これが、リスト10-20のコードをコンパイルしようとしたときにエラーになった理由です。コンパイラはライフタイム省略規則をひととおり適用しましたが、それでもシグネチャ内の参照のライフタイムをすべて導き出せませんでした。
3番目の規則は実際にはメソッドシグネチャにしか適用されないので、次はその文脈でライフタイムを見て、なぜ3番目の規則によってメソッドシグネチャでライフタイムを注釈する必要があまりないのかを確認しましょう。
メソッド定義において
ライフタイムを持つ構造体に対してメソッドを実装するときは、リスト10-11に示したように、ジェネリック型パラメータと同じ構文を使います。ライフタイムパラメータをどこで宣言して使うかは、それらが構造体のフィールドに関連しているのか、それともメソッドのパラメータと戻り値に関連しているのかによって決まります。
構造体フィールドのライフタイム名は、常に impl キーワードの後で宣言し、その後で構造体名の後に使う必要があります。なぜなら、それらのライフタイムは構造体の型の一部だからです。
impl ブロック内のメソッドシグネチャでは、参照が構造体のフィールド内の参照のライフタイムに結び付いている場合もあれば、独立している場合もあります。さらに、ライフタイム省略規則により、メソッドシグネチャではライフタイム注釈が不要になることがよくあります。リスト10-24で定義した ImportantExcerpt という名前の構造体を使ったいくつかの例を見てみましょう。
まず、level という名前のメソッドを使います。このメソッドの唯一のパラメータは self への参照で、戻り値は i32 であり、何かへの参照ではありません。
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
impl の後にあるライフタイムパラメータ宣言と、型名の後でのその使用は必要ですが、最初の省略規則のおかげで、self への参照のライフタイムを注釈する必要はありません。
次は、3番目のライフタイム省略規則が適用される例です。
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
2つの入力ライフタイムがあるため、Rustは最初のライフタイム省略規則を適用し、&self と announcement の両方にそれぞれ独自のライフタイムを与えます。そして、パラメータの1つが &self であるため、戻り値の型は &self のライフタイムを受け取り、すべてのライフタイムが考慮されたことになります。
静的ライフタイム
ここで取り上げる必要のある特別なライフタイムの 1 つが 'static で、これは対象の参照がプログラムの実行期間全体にわたって生存 できる ことを示します。すべての文字列リテラルは 'static ライフタイムを持っており、次のように注釈を付けられます。
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}
この文字列のテキストはプログラムのバイナリに直接格納されており、常に利用可能です。そのため、すべての文字列リテラルのライフタイムは 'static です。
エラーメッセージの中で、'static ライフタイムを使うよう提案されることがあります。しかし、参照のライフタイムとして 'static を指定する前に、その参照が実際にプログラムのライフタイム全体にわたって生存するのか、そしてそうしてよいのかを考えてください。ほとんどの場合、'static ライフタイムを提案するエラーメッセージは、ダングリング参照を作ろうとしているか、利用可能なライフタイムの不一致があることに起因します。そのような場合の解決策は、'static ライフタイムを指定することではなく、それらの問題を修正することです。
ジェネリック型パラメータ、トレイト境界、ライフタイム
ジェネリック型パラメータ、トレイト境界、ライフタイムをすべて 1 つの関数で指定する構文を、簡単に見てみましょう。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {result}");
}
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}
これは、2 つの文字列スライスのうち長いほうを返す、リスト 10-21 の longest 関数です。ただし今度は、ジェネリック型 T の ann という追加のパラメータがあります。この T には、where 句で指定されているように Display トレイトを実装する任意の型を入れることができます。この追加のパラメータは {} を使って表示されるため、Display トレイト境界が必要です。ライフタイムはジェネリックの一種なので、ライフタイムパラメータ 'a とジェネリック型パラメータ T の宣言は、関数名の後の山かっこ内にある同じリストに入ります。
まとめ
この章ではたくさんの内容を扱いました。ジェネリック型パラメータ、トレイトとトレイト境界、そしてジェネリックなライフタイムパラメータについて理解したので、さまざまな状況で動作する、重複のないコードを書けるようになりました。ジェネリック型パラメータを使うと、異なる型に対して同じコードを適用できます。トレイトとトレイト境界は、型がジェネリックであっても、そのコードが必要とする振る舞いを備えていることを保証します。また、ライフタイム注釈を使って、この柔軟なコードにダングリング参照が含まれないようにする方法も学びました。そして、この解析はすべてコンパイル時に行われるため、実行時の性能には影響しません。
信じられないかもしれませんが、この章で扱ったトピックについては、まだまだ学ぶべきことがあります。第 18 章では、トレイトを使うもう 1 つの方法であるトレイトオブジェクトについて説明します。また、ライフタイム注釈に関するより複雑なシナリオもありますが、それらが必要になるのは非常に高度な場面だけです。そのような内容については、Rust Reference を読むとよいでしょう。次は、コードが期待どおりに動作していることを確認できるように、Rust でテストを書く方法を学びます。