Memo

メモ > 技術 > プログラミング言語: Rust > ライフタイム

ライフタイム
&を用いた参照を利用した際に、解放されたメモリを参照してしまうことを避けるための仕組み。 例えば以下のプログラムは、実行すると「error[E0597]: `x` does not live long enough」のエラーになる。 printlnでrを参照する時点で、参照元のxは破棄されて内容を参照できないため。 (なお「r = x;」なら、参照による所有権の移動ではなく複製が行なわれるため、エラーにはならない。)
fn main() { { let r; { let x = 5; r = &x; } println!("r: {r}"); } }
上記はプログラムが単純だが、「&str」で文字列を参照する際に同じ問題が発生する。 RustのLifetimeってなんなん https://zenn.dev/ucwork/articles/6de5c9c2257f2d Rustのライフタイムについて知りたい #Rust - Qiita https://qiita.com/toreis/items/970bcfed6a930e3c84dc Rust の 所有権、借用、ライフタイムについて初心者目線で説明と整理を試みる | blog.ojisan.io https://blog.ojisan.io/rust-ownership-wakaran/ ライフタイム|Rust入門 https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/fbd089 > ライフタイムは&演算子の後ろに指定します.慣例的に a,b,c,… と指定します. ■関数でのライフタイム 以下は文字列の長さを比較するプログラムだが、実行すると「error[E0106]: missing lifetime specifier」のエラーになる。 関数longestにxとyが渡されているが、関数からどちらの値が返ってくるかコンパイル時に確定できないため。
fn main() { let string1 = "ABC"; let string2 = "ABCD"; let result = longest(string1, string2); println!("The longest string is: {result}"); } fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
以下のとおりライフタイム「'a」を設定すると、xとyの両方が同じ期間だけ生きることを宣言でき、エラーにならなくなる。 (実行結果は「The longest string is: ABCD」となる。)
fn main() { let string1 = "ABC"; let string2 = "ABCD"; let result = longest(string1, 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 } }
また以下のように、「参照を含まずにデータを共有する」ように変更することでもエラーを回避できる。 (前述の「借用と参照」にあるコードと同じような内容になる。) ライフタイムの管理が不要になるメリットはあるが、文字列のコピーが発生するのでメモリ使用量が増え、パフォーマンスも落ちる。
fn main() { let string1 = String::from("ABC"); let string2 = String::from("ABCD"); let result = longest(string1, string2); println!("The longest string is: {result}"); } fn longest(x: String, y: String) -> String { if x.len() > y.len() { x } else { y } }
■構造体でのライフタイム 以下は構造体を使用するプログラムだが、実行すると「error[E0106]: missing lifetime specifier」のエラーになる。
struct Person { name: &str, } fn main() { let taro = Person { name: "山田太郎", }; println!("{}さんこんにちは。", taro.name); }
参照を構造体のフィールドとして使う場合、参照の寿命(ライフタイム)を明示的に指定する必要がある。 Rustには「ライフタイム省略規則」があり、関数の引数や戻り値など、明らかな場合だけは自動で補完してくれるが、構造体は対象外となっている。 詳細は後述の「構造体でのライフタイム(ChatGPTによる詳細解説)」も参照。 以下のとおりライフタイム「'a」を設定するとエラーにならなくなる。 (実行結果は「山田太郎さんこんにちは。」となる。)
struct Person<'a> { name: &'a str, } fn main() { let taro = Person { name: "山田太郎", }; println!("{}さんこんにちは。", taro.name); }
また以下のように、「参照を含まずにデータを共有する」ように変更することでもエラーを回避できる。 (前述の「構造体」にあるコードと同じような内容になる。) ライフタイムの管理が不要になるメリットはあるが、文字列のコピーが発生するのでメモリ使用量が増え、パフォーマンスも落ちる。
struct Person { name: String, } fn main() { let taro = Person { name: String::from("山田太郎"), }; println!("{}さんこんにちは。", taro.name); }
■構造体でのライフタイム(ChatGPTによる詳細解説) Rustには「ライフタイム省略規則」があり、関数の引数や戻り値など、明らかな場合だけは自動で補完してくれるが、構造体は対象外となっている。 …について、どうして構造体は対象外なのかをChatGPTに聞いた際の回答。 ↓ 関数ではライフタイムが省略できるのに、構造体ではダメなのはなぜか? ──これはRustの安全性と明確性を両立するための設計方針に深く関わっています。 結論から言うと: 構造体は"データの設計図"であり、その参照の寿命をはっきり明示する責任があるため、ライフタイム省略の対象外にされているのです。 理由?: 構造体は関数と違って「文脈」がない 関数のライフタイム省略は、「関数の引数と戻り値の関係」が分かっているから可能です。 たとえば
fn get_name(name: &str) -> &str { name }
このようなケースでは ・引数のライフタイムが1つしかない → 戻り値のライフタイムはそれと同じでよい ・明確な「借用の流れ」がある → 安全に推論可能 関数には文脈(引数 → 戻り値)があります。 しかし、構造体では
struct Person { name: &str, // ← これ、どのライフタイム? }
この時点では どこから借用してるかも、どう使われるかも全く分からない ため、推論が危険なんです。 たとえばこのPersonが ・一時変数の参照を持っているのか ・グローバルな静的変数を指しているのか ・関数の引数の一部なのか 何の情報もないため、Rustコンパイラは安全に判断できません。 理由?: 明示することで安全性と可読性を保証 構造体は「データ構造を定義する型」なので、長期的に使われる可能性が高いです。 もし構造体がライフタイムを省略できてしまったら ・どの参照がどの寿命に依存しているか分からない ・API設計として不明確 ・ライブラリ使用者にとって危険 となってしまいます。 Rustでは、安全性と同じくらい「明示性」を重視しています。構造体にライフタイムを明示させることで ・使う側もライフタイムを意識する ・借用関係を設計レベルで可視化できる ・コンパイルエラーで「何がどの寿命に依存しているか」が明確になる といった利点があるのです。 Rustは「曖昧さより明示性」を選ぶ言語です。 短期的な書きやすさよりも、長期的なバグのなさと可読性を優先しています。 ■その他 初期はライフタイムの明記が必須だったらしいが、今は以下の場合にライフタイムを明記しなくてもいいようになっている…らしい。 構造体のメソッドはselfを持つことが多いので、ライフタイムの指定は不要になることが多い…らしい。 ・参照の各引数は、独自のライフタイム引数を得る。 ・1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入される。 ・selfのあるメソッドの場合、selfのライフタイムが全出力ライフタイム引数に代入される。 以下は構造体のメソッドfirst_charを定義しているが、このように「self」が渡されることが多い。
struct Person<'a> { name: &'a str, } impl<'a> Person<'a> { fn first_char(&self) -> Option<char> { self.name.chars().next() } } fn main() { let taro = Person { name: "山田太郎", }; println!("{}さんこんにちは。", taro.name); if let Some(first) = taro.first_char() { println!("先頭の文字は{}です。", first); } }
以下メモ。 ・「'a」という記述があるが、これはライフタイム指定でジェネリクスの一種。 ライフタイムは、Rustの借用チェッカーが参照の有効期間を検証するための仕組み。 「a」という名前は、「src」など任意のものを付けられる。 例えば「Value<'src>」の場合、列挙型「Value」内で参照を持つフィールドのライフタイムが「src」であることを示している。 ・ライフタイムは、コンパイラが参照の有効期間を追跡し、メモリ安全性を保障する。 複数の参照が同じライフタイムを共有するか、または別々のライフタイムを持つかを明示することで、より厳密なメモリ管理を行なう。 ・コンパイラは多くの場合、自動的にライフタイムを推測することができるが、特に複雑なケースでは明示的なライフタイム指定が必要になる。 また構造体や列挙型のような型定義で参照を含む場合、ライフタイム指定が必須になる。 ・ライフタイムは他の言語に似た概念が存在しない。 ・多用するとプログラムが読みづらくなるので、指定は最低限に…というもの?

Advertisement