Rustのライフタイムって何よ
※この記事は初心者がRustのライフタイムを理解したい私が、Qiitaにアウトプットしながら覚えた記事になります。
こんにちは、Rustを触り始めてN日のとれいす(@toreis)です。
The Rust Programming Languageを読んでいて、こんな記述がありました。
ライフタイムの概念は、他のプログラミング言語の道具とはどこか異なり、間違いなくRustで一番際立った機能になっています。 この章では、ライフタイムの全体を解説することはしませんが、 ライフタイム記法が必要となる最も一般的な場合について議論しますので、ライフタイムの概念について馴染むことができるでしょう。
私「???」
全く意味がわからないので、使用例のコードを見ていきましょう。
動かないコード
fn main() {
{
let r;
{
let x = 5; // xがここにある
r = &x;
} // ここでxがスコープから外れる
println!("r: {}", r); // xはもう捨ててあるので、rが参照する先はない -> エラー
}
}
これをライフタイム的に見てみましょう。
fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}
'bを超えてrを参照してしまっているので、エラーが出るわけですね。
ここまでは直感的にわかります。
問題は次です。
関数のジェネリックなライフタイム
// https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
// 最長の文字列は、{}です
println!("The longest string is {}", result);
}
さて、ここでlongest関数は1つめ、2つめに渡された引数のうち、一番長い文字列を持つ方を返すということがわかりますね。
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
(エラー[E0106]: ライフタイム指定子が不足しています)
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ^ expected 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`
(助言: この関数の戻り値型は借用された値を含んでいますが、
シグネチャは、それが`x`と`y`どちらから借用されたものなのか宣言していません)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
こんなエラーがでるんですね。
助言を片っ端から見ていきましょう。
「この関数の戻り値型は借用された値を含んでいますが、
シグネチャは、それがx
とy
どちらから借用されたものなのか宣言していません」
「この関数の戻り値型は借用された値を含んでいますが」
戻り値の「&str」がそういうことですね。
「シグネチャは、それがx
とy
どちらから借用されたものなのか宣言していません」
私「なに、どういうこと」
そもそもシグネチャとは:
関数と引数、数やその型、戻り値の型のこと
だそうです。
要は
fn longest(x: &str, y: &str) -> &str
の中で宣言された戻り値となる&str
は、x
が帰ってくるのかy
が帰ってくるのか、コンパイラはわかりません。
ライフタイム注釈
ここで活躍するのがライフタイム注釈です。
公式ではこのように紹介されています。
ライフタイム注釈は、いかなる参照の生存期間も変えることはありません。シグニチャにジェネリックな型引数を指定された 関数が、あらゆる型を受け取ることができるのと同様に、ジェネリックなライフタイム引数を指定された関数は、 あらゆるライフタイムの参照を受け取ることができます。ライフタイム注釈は、ライフタイムに影響することなく、 複数の参照のライフタイムのお互いの関係を記述します。
前半の理解をします。
「シグニチャにジェネリックな型引数を指定された関数が、あらゆる型を受け取ることができるのと同様」
use std::fmt;
fn function<T>(thing: T) where T: fmt::Display {
println!("{}", thing);
}
fn main() {
let a = 3;
function(a);
let b = String::from("Test");
function(b);
}
ジェネリックの話はこういうことですよね。
where句は気にしないでください。(ジェネリックとして受け取る型Tに、println!()を使えるようにしてくださいという制限です。)
こんな感じで、ライフタイム注釈を使ってみると、次のようになるでしょう。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
ライフタイム注釈が'a
として表現されています。
xのライフタイムが'a
、またyのライフタイムも'a
で表されています。
さらに戻り値のライフタイムも、xのライフタイム、yのライフタイムと同じ'a
で返されています。
これで関数シグニチャは、何らかのライフタイム'aに対して、関数は2つの引数を取り、 どちらも少なくともライフタイム'aと同じだけ生きる文字列スライスであるとコンパイラに教えるようになりました。 また、この関数シグニチャは、関数から返る文字列スライスも少なくともライフタイム'aと同じだけ生きると、 コンパイラに教えています。 実際には、longest関数が返す参照のライフタイムは、渡された参照のうち、小さい方のライフタイムと同じであるという事です。 これらの制約は、まさに私たちがコンパイラに保証してほしかったものです。
from https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html
ということです。
つまり
fn main() {
// 長い文字列は長い
let string1 = String::from("long string is long");
// (訳注:この言葉自体に深い意味はない。下の"xyz"より長いということだけが重要)
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("{}", result);
// 一番長い文字列は{}
}
// println!("The longest string is {}", result);
// ここではもうstring2のライフタイムが切れてるので、上をアンコメントするとエラーが出る
}
ということですね。
私「なるほど!!」
そして、こういうことはできません。
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()
}
これはlongest
に渡されたstrのライフタイムに関係なく、result
という引数で渡されたものに関係ないライフタイムを持つ変数を返すため、エラーを起こします。
構造体を使う時
構造体に参照を使う時はどのように書くのでしょうか。
TRPLではこのように書いてあります。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
// 僕をイシュマエルとお呼び。何年か前・・・
let novel = String::from("Call me Ishmael. Some years ago...");
// "'.'が見つかりませんでした"
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
まあ、そのままな気がしますね。
ライフタイム省略
ライフタイムを書かなくても、TRPLの4章では動いているコードをたくさん見ました!
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[..]
}
このfirst_word関数では、引数も戻り値も参照であるにもかかわらずライフタイム注釈がついていません。
これは、Rustが出来上がっていく過程で省略できるパターンが確立?されたようです。
前提
関数やメソッドの引数のライフタイムは、入力ライフタイムと呼ばれ、 戻り値のライフタイムは出力ライフタイムと称されます。
- 1引数の関数は、1つのライフタイム引数を得る
- 1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入される
- 複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&selfや&mut selfだったら、 selfのライフタイムが全出力ライフタイム引数に代入される
上から順に、私にわかるように説明します。
1引数の関数は、1つのライフタイム引数を得る
1引数なら
fn foo<'a>(x: &'a i32)
と、'a
を使っていますね。
2つなら
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
と、'a
、そして'b
が使われています。
このように、3つなら3つ...となっていくようです。
1つだけ入力ライフタイム引数があるなら、そのライフタイムがすべてその出力ライフタイム引数に代入される
fn foo<'a>(x: &'a i32) -> &'a i32
言われてみりゃそらそうかと思っています。
別の書き方としてはこんなのもあります。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
さっき見ましたね。
xもyも同じライフタイムを持ち(正確にはxとyのどちらか短い方のライフタイムだと思っています)、戻り値も同じライフタイムで返しています。
公式では以下のように書いてあります。
長いライフタイムは、短いものに圧縮(coerce)することで、そのままでは動作しないスコープの中でも使用できるようになります。これは、Rustコンパイラが推論の結果として圧縮する場合と、複数のライフタイムを比較して圧縮する場合があります。
複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&selfや&mut selfだったら、 selfのライフタイムが全出力ライフタイム引数に代入される
これは次でもっと詳しくお話します。
メソッド定義におけるライフタイム注釈
構造体に参照を含むメソッドを実装したいときの話です。
これは一旦コードを見た方が理解が早かったです。
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().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
上から3つめの宣言ブロックを見てみてください。
fn announce_and_return_part(&self, announcement: &str) -> &str
と書いてありますが、入力はどっちも参照ですね。
出力も参照なので、本来ならライフタイムがついていなきゃコンパイルに失敗してしまうのでは?と思うでしょう。
さっきの3番目を思い出してください。
複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&selfや&mut selfだったら、 selfのライフタイムが全出力ライフタイム引数に代入される
です。
つまり入力引数に&self
があるので、&self
のライフタイムを引き継いで出力されます。
静的ライフタイム
これは、全期間で生存できるという代物です。
使い方は至って簡単で、'static
をつけるだけとなります。
// 僕は静的ライフタイムを持ってるよ
let s: &'static str = "I have a static lifetime.";
発展: バケモンみたいな宣言をする
ジェネリックな型引数、トレイト境界、ライフタイム指定の構文のすべてを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
}
}
関数名のあとに<'a, T>
を使ってライフタイム指定、ジェネリックが指定されています。
そのあと、where T: Display
で、Tに入る型はDisplay
が実装されていることを強制しています。
関数のなかでprintln!を使っているからですね。
かなり密度がすごいですが、上が理解できていれば確かにそうなるなあ、となると思います。
最後に
頭が痛くなりましたが、よく理解できました。
よくわかんないなーってなっている方への助けになることを願います。
変更履歴
2023.2.10 ライフタイムの圧縮について明記