Chapter 2: 複雑なライフタイム問題
学習目標
- 複数のライフタイムパラメータの扱いを習得する
- ライフタイム境界の高度な使い方を学ぶ
- 関数ポインタとクロージャのライフタイムを理解する
- GAT (Generic Associated Types) を活用する
- 実践的な難問とその解決策を学ぶ
---
2.1 複数のライフタイムパラメータ
2.1.1 独立したライフタイム
struct Context<'s, 'c> {
// ^^ ^^
// | コンテキストのライフタイム
// 文字列のライフタイム
string_data: &'s str,
config: &'c Config,
}
impl<'s, 'c> Context<'s, 'c> {
fn new(s: &'s str, c: &'c Config) -> Self {
Context {
string_data: s,
config: c,
}
}
// 's と 'c は独立
fn get_string(&self) -> &'s str {
self.string_data
}
fn get_config(&self) -> &'c Config {
self.config
}
}
2.1.2 ライフタイムの順序制約
struct Parser<'a, 'b> where 'b: 'a {
// ^^^^^^^
// 'b は 'a より長生き
input: &'a str,
cache: &'b Cache,
}
// なぜこの制約が必要か
impl<'a, 'b> Parser<'a, 'b>
where
'b: 'a,
{
fn parse(&self) -> Result<&'a str, Error> {
// cache('b)を使って input('a)を処理
// cacheはinputより長生きする必要がある
Ok(self.input)
}
}
---
2.2 高階関数とライフタイム
2.2.1 関数を返す関数
// 間違った例
fn returns_closure() -> Box<dyn Fn(&str) -> &str> {
// ^^^^ ^^^^
// ライフタイムが不明!
Box::new(|x| x)
}
// 正しい例: HRTB使用
fn returns_closure() -> Box<dyn for<'a> Fn(&'a str) -> &'a str> {
Box::new(|x| x)
}
2.2.2 クロージャとキャプチャ
fn example() {
let string = String::from("hello");
// クロージャが string を借用
let closure = || {
println!("{}", string);
};
// string のライフタイムはクロージャの存在期間を含む必要
closure();
}
// より複雑な例
fn complex_closure<'a>(s: &'a str) -> impl Fn() -> &'a str {
move || s
}
---
2.3 ライフタイム省略の落とし穴
2.3.1 予期しない省略
// これは...
fn foo(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// こう解釈される(エラー!)
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// ^^^^
// どのライフタイム?
}
// 正しく書く
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
2.3.2 メソッドでの省略
struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
// 省略前
fn parse<'b>(&'b self) -> &'a str {
self.input
}
// 省略後(推論される)
fn parse(&self) -> &'a str {
// ^^^^^^
// &'b self と推論されるが、
// 戻り値は 'a を使う
self.input
}
}
---
2.4 Generic Associated Types (GAT)
2.4.1 LendingIterator
// 標準の Iterator
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
// GAT を使った LendingIterator
trait LendingIterator {
type Item<'a> where Self: 'a;
// ^^^^
// ライフタイム付き関連型
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
// 実装例
impl<'s, T> LendingIterator for WindowsMut<'s, T> {
type Item<'a> = &'a mut [T] where Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
// スライスを borrowing して返せる
None // 実装省略
}
}
2.4.2 GATの応用
trait Database {
type Transaction<'a> where Self: 'a;
fn begin<'a>(&'a mut self) -> Self::Transaction<'a>;
}
struct MyDb;
impl Database for MyDb {
type Transaction<'a> = Transaction<'a>;
fn begin<'a>(&'a mut self) -> Transaction<'a> {
Transaction { db: self }
}
}
struct Transaction<'a> {
db: &'a mut MyDb,
}
---
2.5 自己参照とPin
2.5.1 問題の本質
struct SelfRef {
data: String,
ptr: *const String, // data を指す
}
fn broken() {
let mut s = SelfRef {
data: String::from("hello"),
ptr: std::ptr::null(),
};
s.ptr = &s.data as *const _;
// 問題: s をムーブすると ptr が無効に!
let s2 = s; // s.ptr は古いアドレスを指す
}
2.5.2 Pin による解決
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned,
}
impl SelfRef {
fn new(data: String) -> Pin<Box<Self>> {
let mut boxed = Box::pin(SelfRef {
data,
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
let self_ptr: *const String = &boxed.data;
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).ptr = self_ptr;
}
boxed
}
fn get_data(self: Pin<&Self>) -> &str {
&self.data
}
fn get_ptr_data(self: Pin<&Self>) -> &str {
unsafe { &*self.ptr }
}
}
---
2.6 実践的な難問
2.6.1 キャッシュとライフタイム
問題:
struct Cache {
data: HashMap<String, String>,
}
impl Cache {
// エラー:どのライフタイム?
fn get_or_insert(&mut self, key: &str, default: &str) -> &str {
if let Some(value) = self.data.get(key) {
value // ライフタイム 'a
} else {
self.data.insert(key.to_string(), default.to_string());
self.data.get(key).unwrap() // ライフタイム 'b
}
}
}
解決策1:ライフタイムを明示
impl Cache {
fn get_or_insert<'a>(&'a mut self, key: &str, default: &str) -> &'a str {
if !self.data.contains_key(key) {
self.data.insert(key.to_string(), default.to_string());
}
self.data.get(key).unwrap()
}
}
解決策2:エントリーAPI
impl Cache {
fn get_or_insert<'a>(
&'a mut self,
key: &str,
default: &str,
) -> &'a str {
self.data
.entry(key.to_string())
.or_insert_with(|| default.to_string())
}
}
2.6.2 イテレータの連鎖
問題:
struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
fn parse(&mut self) -> impl Iterator<Item = &str> {
// ^^^^
// ライフタイム不明
self.input.split_whitespace()
}
}
解決策:
impl<'a> Parser<'a> {
fn parse<'b>(&'b mut self) -> impl Iterator<Item = &'a str> + 'b {
// ^^ ^^ ^^
// メソッドのライフタイム
// inputのライフタイム
// イテレータの境界
self.input.split_whitespace()
}
}
---
2.7 ライフタイムサブタイピングの高度な使用
2.7.1 共変性の活用
fn extend_lifetime<'a, 'b>(r: &'a &'b i32) -> &'a &'a i32
where
'b: 'a,
{
// &'b i32 は &'a i32 のサブタイプ('b: 'a のとき)
unsafe { std::mem::transmute(r) }
// 実際には transmute は不要(共変性で自動変換)
}
2.7.2 反変性の理解
fn shorten_fn_lifetime<'a, 'b>(
f: fn(&'b i32),
) -> fn(&'a i32)
where
'a: 'b,
{
// fn(&'b i32) は fn(&'a i32) のサブタイプ('a: 'b のとき)
f
}
---
2.8 デバッグテクニック
2.8.1 ライフタイムの明示
// 型エラーが出たら、全てのライフタイムを明示して理解する
fn debug_lifetimes<'a, 'b, 'c>(
x: &'a str,
y: &'b str,
z: &'c str,
) -> &'a str {
// どのライフタイムが問題か明確に
x
}
2.8.2 rustc の詳細エラー
$ rustc --explain E0623
# または
$ cargo clippy -- -W clippy::needless_lifetimes
---
2.9 まとめ
2.9.1 複雑なライフタイム問題の解決戦略
1. ライフタイムを全て明示して理解
2. 各参照がどこから来てどこに行くかトレース
3. 省略規則を理解して不要な注釈を削除
4. コンパイラのエラーメッセージを熟読
5. Pin/GAT などの高度な機能を検討
2.9.2 よくあるパターン
---
練習問題
問題1
複数のライフタイムを持つ構造体を設計し、それぞれの役割を説明しなさい。問題2
LendingIterator を実装し、標準の Iterator との違いを説明しなさい。問題3
なぜ&'a mut T が invariant なのか、variance の観点から説明しなさい。---