Chapter 3: ライフタイム基礎
この章の目標
この章を読み終えると、以下のことが理解できるようになります:
- ライフタイムとは何か、なぜ必要なのか
- ライフタイム注釈の基本的な書き方
- ライフタイムエリジョンの3つのルール
- ライフタイムのサブタイプ関係
- 構造体とライフタイム
- よくあるライフタイムエラーとその修正方法
ライフタイムとは
1.1 ライフタイムの定義
ライフタイムは、参照が有効である期間を表す型レベルの情報です。
fn main() {
let s = String::from("hello");
// └── 's のライフタイム開始
let r = &s;
// └── 'r のライフタイム開始('s の部分集合)
println!("{}", r);
// └── 'r のライフタイム終了
}
// └── 's のライフタイム終了
重要な点:
1.2 なぜライフタイムが必要なのか
ライフタイムがないと、ダングリングポインタを防げません:
// ライフタイムチェックなしだとしたら...
fn dangle() -> &String {
let s = String::from("hello");
&s // sはここで破棄される → ダングリングポインタ!
}
Rustはライフタイムチェックで、このようなコードを拒否します:
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:3:5
|
3 | &s
| ^^ returns a reference to data owned by the current function
2. ライフタイム注釈の基礎
2.1 基本的な構文
ライフタイム注釈は、' で始まる識別子です:
&'a T // 不変参照、ライフタイム 'a
&'a mut T // 可変参照、ライフタイム 'a
命名規則:
'a, 'b, 'c が一般的'static は特別なライフタイム(プログラム全体)'input, 'output2.2 関数のライフタイム注釈
例1: 最も長い文字列を返す
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
解説:
<'a>: ライフタイムパラメータの宣言
x: &'a str: xのライフタイムは'a
y: &'a str: yのライフタイムは'a
-> &'a str: 戻り値のライフタイムは'a
意味: 戻り値のライフタイムは、xとyの短い方
使用例:
fn main() {
let s1 = String::from("long string");
let s2 = String::from("short");
let result = longest(&s1, &s2);
println!("Longest: {}", result);
}
例2: ライフタイムが異なる場合
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
// エラー!resultのライフタイムがs2より長い
}
println!("Longest: {}", result);
}
エラーメッセージ:
error[E0597]: `s2` does not live long enough
--> src/main.rs:6:30
|
6 | result = longest(&s1, &s2);
| ^^^^ borrowed value does not live long enough
7 | }
| - `s2` dropped here while still borrowed
8 | println!("Longest: {}", result);
| ------ borrow later used here
2.3 複数のライフタイムパラメータ
異なるライフタイムを持つ参照を扱う場合:
fn first_word<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x.split_whitespace().next().unwrap()
}
解説:
'a と 'b は独立したライフタイム
戻り値は 'a(xのライフタイム)
yのライフタイム 'b は戻り値に影響しない
2.4 ライフタイムの制約
ライフタイム間に制約を付けることができます:
fn announce_and_return<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b は 'a より長い(またはイコール)
{
println!("Announcement: {}", y);
x
}
記号の意味:
'b: 'a ← 'b は 'a よりアウトライブ(長生き)する
= 'b ⊇ 'a
= 'b のライフタイムは 'a 以上
3. ライフタイムエリジョン
3.1 エリジョンとは
ライフタイムエリジョンは、明示的なライフタイム注釈を省略できるルールです。
// エリジョンあり(書かなくて良い)
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap()
}
// エリジョンなし(明示的)
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap()
}
// 両方同じ意味
3.2 エリジョンの3つのルール
コンパイラは、以下の3つのルールでライフタイムを推論します:
ルール1: 各入力パラメータに異なるライフタイムを割り当てる
fn foo(x: &i32)
// ↓ コンパイラが推論
fn foo<'a>(x: &'a i32)
fn bar(x: &i32, y: &i32)
// ↓ コンパイラが推論
fn bar<'a, 'b>(x: &'a i32, y: &'b i32)
ルール2: 入力ライフタイムが1つなら、出力に適用
fn foo(x: &i32) -> &i32
// ↓ コンパイラが推論
fn foo<'a>(x: &'a i32) -> &'a i32
ルール3: 入力に &self または &mut self があれば、そのライフタイムを出力に適用
impl Foo {
fn bar(&self, x: &i32) -> &i32
// ↓ コンパイラが推論
fn bar<'a, 'b>(&'a self, x: &'b i32) -> &'a i32
}
3.3 エリジョンが適用されない例
// エラー!ルールでは推論できない
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
なぜエラー?
ルール1適用: fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
ルール2適用: 入力が2つ → 適用できない
ルール3適用: selfなし → 適用できない
結論: 出力のライフタイムが決まらない → エラー
修正方法:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
4. 構造体とライフタイム
4.1 基本的な構造体
構造体が参照を持つ場合、ライフタイム注釈が必要です:
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 excerpt = ImportantExcerpt {
part: first_sentence,
};
}
解説:
<'a>: 構造体のライフタイムパラメータ
part: &'a str: partのライフタイムは'a
意味: ImportantExcerptのインスタンスは、
partが指すデータより長生きできない
4.2 複数のライフタイム
struct Context<'a, 'b> {
first: &'a str,
second: &'b str,
}
fn main() {
let first = String::from("first");
{
let second = String::from("second");
let ctx = Context {
first: &first,
second: &second,
};
// ctxはここでドロップされる
}
}
4.3 構造体のメソッドとライフタイム
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
エリジョン適用:
// エリジョン前(明示的)
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(
&'a self,
announcement: &'b str,
) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
}
// エリジョン後(省略形)
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
5. ライフタイムのサブタイプ関係
5.1 サブタイプとは
ライフタイムにもサブタイプ関係があります:
'a: 'b ← 'a は 'b のサブタイプ
= 'a は 'b より長い(またはイコール)
= 'a ⊇ 'b
直感的な理解:
長いライフタイムは、短いライフタイムの「サブタイプ」
↓
長いライフタイムの参照は、短いライフタイムが期待される場所で使える
5.2 共変性(Covariance)
fn foo<'a>(x: &'a str) -> &'a str {
x
}
fn main() {
let long_lived = String::from("long");
{
let short_lived = String::from("short");
let result = foo(&long_lived); // OK
// 'long > 'short だが、foo は 'short でも動く
}
}
5.3 反変性(Contravariance)
関数ポインタの引数は反変です:
fn apply<'a>(f: fn(&'a str), s: &'a str) {
f(s)
}
fn print_short(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
apply(print_short, &s); // OK
}
6. 'static ライフタイム
6.1 'static とは
'static は、プログラム全体のライフタイムです:
let s: &'static str = "hello";
特徴:
'static6.2 'static の使用例
例1: 定数
const HELLO: &'static str = "Hello, world!";
fn main() {
println!("{}", HELLO);
}
例2: スレッド間の共有
use std::thread;
static GLOBAL: &str = "global";
fn main() {
thread::spawn(|| {
println!("{}", GLOBAL); // OK
});
}
6.3 'static の誤解
誤解: すべての参照を 'static にすれば良い
真実: 'static は非常に制限的
// これはエラー
fn dangle() -> &'static String {
let s = String::from("hello");
&s // エラー!sは'staticではない
}
7. よくあるライフタイムエラー
7.1 戻り値の参照が短命
// エラー
fn dangle() -> &String {
let s = String::from("hello");
&s
}
// 修正1: 所有権を返す
fn fixed1() -> String {
let s = String::from("hello");
s
}
// 修正2: 'static を使う
fn fixed2() -> &'static str {
"hello"
}
7.2 構造体のライフタイム忘れ
// エラー
struct Excerpt {
part: &str,
}
// 修正
struct Excerpt<'a> {
part: &'a str,
}
7.3 ライフタイムの推論失敗
// エラー
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// 修正
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
8. 実践的なパターン
8.1 オプショナルライフタイム
struct Config<'a> {
query: &'a str,
filename: &'a str,
}
impl<'a> Config<'a> {
fn new(args: &'a [String]) -> Result<Config<'a>, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let filename = &args[2];
Ok(Config { query, filename })
}
}
8.2 イテレータとライフタイム
struct Lines<'a> {
text: &'a str,
}
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.text.is_empty() {
return None;
}
if let Some(pos) = self.text.find('\n') {
let line = &self.text[..pos];
self.text = &self.text[pos + 1..];
Some(line)
} else {
let line = self.text;
self.text = "";
Some(line)
}
}
}
まとめ
重要なポイント
実践的なアドバイス
'static は慎重に使う次の章へ
次の章では、ライフタイム応用として、より高度なパターン(高階ライフタイム、ファントムライフタイム、ライフタイムのサブタイピング)を学びます。