Chapter 2: 複雑なライフタイム
学習目標
この章を読み終えると、以下のことができるようになります:
- 複数のライフタイムパラメータを持つ関数を設計できる
- ライフタイム省略規則を完全に理解し、予測できる
- ライフタイム境界(
T: 'a)を適切に使える - Higher-Ranked Trait Bounds(
for<'a>)を実戦で活用できる - ジェネリック型におけるライフタイムの変性を理解する
- Rust標準ライブラリの複雑なライフタイムパターンを読み解ける
- プロダクションコード(Kubernetes、tokio、serde)のライフタイム設計を分析できる
1. 複数のライフタイムパラメータ
1.1 基本パターン
単一のライフタイムパラメータでは表現できない複雑な関係を扱います。
// パターン1: 独立した2つのライフタイム
fn choose<'a, 'b>(x: &'a str, y: &'b str, first: bool) -> (&'a str, &'b str) {
// 'a と 'b は独立している
// 異なるライフタイムを持つ2つの参照を返せる
if first {
(x, y)
} else {
(x, y) // 順序は変えられない(型が異なるため)
}
}
fn main() {
let s1 = String::from("hello"); // 長いライフタイム
let result;
{
let s2 = String::from("world"); // 短いライフタイム
let (r1, r2) = choose(&s1, &s2, true);
println!("{} {}", r1, r2);
} // s2 はここで終了
// r2 も無効になる
// println!("{}", result); // エラー
}
視覚化:
時間軸 ────────────────────────────►
's1: ┌──────────────────────────────┐
│ │
's2: │ ┌──────────┐ │
│ │ │ │
└────┴──────────┴──────────────┘
↑ ↑
r1 ('a) r2 ('b)
独立したライフタイム: 'a と 'b は無関係
1.2 包含関係のある複数ライフタイム
// パターン2: 'a outlives 'b
fn select<'a, 'b>(x: &'a str, y: &'b str, flag: bool) -> &'b str
where
'a: 'b, // 'a は 'b より長く生きる
{
// xのライフタイムは'a、yは'b
// 戻り値は'bなので、両方返せる('aは'bより長い)
if flag {
x // OK: &'a str は &'b str として扱える(covariant)
} else {
y // OK: 同じ型
}
}
fn main() {
let long = String::from("long-lived"); // 'a: 長い
let short;
{
let temp = String::from("short-lived"); // 'b: 短い
short = select(&long, &temp, true);
println!("{}", short);
} // 'b の終了
// short はここでは使えない
}
視覚化:
時間軸 ────────────────────────────►
'a: ┌──────────────────────────────┐
│ │
'b: │ ┌──────────┐ │
│ │ │ │
└────┴──────────┴──────────────┘
'a: 'b が成立('a は 'b を包含)
戻り値は 'b → 'a も 'b も返せる
1.3 共通ライフタイムの計算
// パターン3: 最小共通ライフタイム
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 'a は x と y の最小共通ライフタイム
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
// 'a = 's1 ∩ 's2 = 's2(短い方)
result = longest(&s1, &s2);
println!("{}", result);
} // 's2 の終了 = 'a の終了
// println!("{}", result); // エラー!'a は終了している
}
形式的定義:
'a = 's1 ∩ 's2
where:
∩: ライフタイムの積(最小共通期間)
'a ⊆ 's1 ∧ 'a ⊆ 's2
∀'x. ('x ⊆ 's1 ∧ 'x ⊆ 's2) ⇒ 'x ⊆ 'a
1.4 構造体での複数ライフタイム
// 2つの異なるライフタイムを持つ構造体
struct RefPair<'a, 'b> {
first: &'a str,
second: &'b str,
}
impl<'a, 'b> RefPair<'a, 'b> {
// コンストラクタ: 2つの独立したライフタイム
fn new(first: &'a str, second: &'b str) -> Self {
RefPair { first, second }
}
// 'a を返すメソッド
fn get_first(&self) -> &'a str {
self.first
}
// 'b を返すメソッド
fn get_second(&self) -> &'b str {
self.second
}
// 自己参照のライフタイムも考慮
fn get_first_ref<'s>(&'s self) -> &'s &'a str {
// 戻り値のライフタイムは 's
// しかし内容は 'a
&self.first
}
}
fn main() {
let long = String::from("long-lived");
let pair;
{
let short = String::from("short-lived");
pair = RefPair::new(&long, &short);
// pair 全体のライフタイムは 'short まで
println!("{} {}", pair.first, pair.second);
} // short が終了 → pair も使えなくなる
// println!("{}", pair.first); // エラー
}
ライフタイム制約:
RefPair の有効期間:
min('a, 'b) まで
理由:
構造体のライフタイムは、全てのフィールドの
ライフタイムの最小値
---
2. ライフタイム省略規則(Elision Rules)の詳細
2.1 3つの省略規則
Rustコンパイラは、以下の規則でライフタイムを自動推論します。
// ────────────────────────────────────────────────────
// 規則1: 各入力参照に異なるライフタイムを割り当てる
// ────────────────────────────────────────────────────
// 書いたコード
fn first_word(s: &str) -> &str { /* ... */ }
// コンパイラが補完
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
// ────────────────────────────────────────────────────
// 規則2: 入力ライフタイムが1つなら、出力に伝播
// ────────────────────────────────────────────────────
// 書いたコード
fn identity(x: &i32) -> &i32 { x }
// コンパイラが補完
fn identity<'a>(x: &'a i32) -> &'a i32 { x }
// ────────────────────────────────────────────────────
// 規則3: メソッドの場合、&self のライフタイムを出力に伝播
// ────────────────────────────────────────────────────
// 書いたコード
impl<T> MyStruct<T> {
fn get_ref(&self) -> &T { &self.value }
}
// コンパイラが補完
impl<T> MyStruct<T> {
fn get_ref<'a>(&'a self) -> &'a T { &self.value }
}
2.2 省略できないケース
// ケース1: 複数の入力、1つの出力
// → どのライフタイムを返すか不明確
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 }
}
// ケース2: 関数ポインタ/クロージャ
// → 省略規則が適用されない
fn apply(f: fn(&i32) -> &i32, x: &i32) -> &i32 { // エラー!
f(x)
}
// 正しい書き方: HRTBs使用
fn apply<F>(f: F, x: &i32) -> &i32
where
F: for<'a> Fn(&'a i32) -> &'a i32,
{
f(x)
}
// ケース3: トレイトメソッドでの複雑な関係
trait Parser {
fn parse(&self, input: &str) -> Result<&str, &str>; // エラー!
}
// 正しい書き方
trait Parser {
fn parse<'a, 'b>(&'a self, input: &'b str) -> Result<&'b str, &'b str>;
}
2.3 省略規則の形式的定義
アルゴリズム:
1. 全ての入力参照にライフタイムパラメータを割り当て
fn foo(x: &T, y: &U)
→ fn foo<'a, 'b>(x: &'a T, y: &'b U)
2. 入力ライフタイムが1つの場合、全ての出力に伝播
fn foo<'a>(x: &'a T) -> &U
→ fn foo<'a>(x: &'a T) -> &'a U
3. 入力に &self または &mut self がある場合、
そのライフタイムを全ての出力に伝播
fn foo<'a>(&'a self, x: &T) -> &U
→ fn foo<'a, 'b>(&'a self, x: &'b T) -> &'a U
4. 上記で決まらない場合、エラー
2.4 実践的な例
// 標準ライブラリの例: str::split
impl str {
// 書かれているコード
pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P>
where
P: Pattern<'a>,
{
// ...
}
// 省略規則を適用すると
pub fn split<P>(&self, pat: P) -> Split<'_, P>
where
P: Pattern,
{
// '_: 省略されたライフタイム
}
}
// 使用例
fn main() {
let s = String::from("hello world");
let words: Vec<&str> = s.split(' ').collect();
// words の要素のライフタイムは 's と同じ
}
---
3. ライフタイム境界(Lifetime Bounds): T: 'a
3.1 基本概念
// T: 'a の意味:
// 「型Tは少なくともライフタイム'aの間有効である」
// 例1: ジェネリック構造体
struct Ref<'a, T>
where
T: 'a, // T は 'a より長く生きる
{
value: &'a T,
}
// なぜ必要?
// &'a T という参照を持つためには、T 自体が 'a より長生きしないといけない
視覚化:
時間軸 ────────────────────────────►
'a: ┌──────────────┐
│ │
T: ┌──────────────────────┐
│ │ │
└──────────────┴───────┘
↑
T は 'a を包含
T: 'a が成立
3.2 実践例
// 例2: 参照を持つジェネリック関数
fn store<'a, T>(value: &'a T) -> Box<dyn Fn() -> &'a T + 'a>
where
T: 'a, // T は 'a より長く生きる必要がある
{
// クロージャが 'a の参照を返すため、
// T も 'a より長生きしないといけない
Box::new(move || value)
}
// 例3: 構造体のimpl
impl<'a, T> Ref<'a, T>
where
T: 'a,
{
fn new(value: &'a T) -> Self {
Ref { value }
}
fn get(&self) -> &'a T {
self.value
}
}
fn main() {
let x = 42;
let r = Ref::new(&x);
println!("{}", r.get());
}
3.3 静的ライフタイム境界: T: 'static
// 'static: プログラム全体で有効
// T: 'static の意味: T は参照を含まない、または 'static 参照のみ
// 例1: スレッドに送れる型
use std::thread;
fn spawn_thread<T>(value: T)
where
T: Send + 'static, // T は 'static でないといけない
{
thread::spawn(move || {
// value を使用
println!("In thread: {:?}", value);
});
}
// OK: i32 は参照を含まないので 'static
spawn_thread(42);
// OK: String は所有型なので 'static
spawn_thread(String::from("hello"));
// エラー: &str は非 'static 参照
let s = String::from("local");
// spawn_thread(&s); // エラー!&str は 'static ではない
// OK: &'static str は 'static
spawn_thread("static string literal");
3.4 複雑な境界
// 複数のライフタイム境界
struct Complex<'a, 'b, T, U>
where
T: 'a, // T は 'a より長生き
U: 'b, // U は 'b より長生き
'a: 'b, // 'a は 'b を包含
{
t_ref: &'a T,
u_ref: &'b U,
}
impl<'a, 'b, T, U> Complex<'a, 'b, T, U>
where
T: 'a,
U: 'b,
'a: 'b,
{
fn new(t: &'a T, u: &'b U) -> Self {
Complex { t_ref: t, u_ref: u }
}
// 't は 'a より短い
fn get_t<'t>(&'t self) -> &'t T
where
'a: 't, // 'a outlives 't
{
self.t_ref
}
}
---
4. Higher-Ranked Trait Bounds(HRTBs)の実践
4.1 関数ポインタとクロージャ
// 問題: 任意のライフタイムで動作するクロージャを受け取りたい
// 間違い: ライフタイムが固定される
fn call_with_ref<'a, F>(f: F, x: &'a i32)
where
F: Fn(&'a i32) -> &'a i32, // 'a は関数シグネチャで決まる
{
f(x);
}
// 正しい: HRTBs使用
fn call_with_ref_hrtb<F>(f: F, x: &i32)
where
F: for<'a> Fn(&'a i32) -> &'a i32, // 任意の 'a で動作
{
f(x);
}
fn main() {
let identity = |x: &i32| x;
let value = 42;
call_with_ref_hrtb(identity, &value);
}
4.2 トレイトオブジェクトでのHRTBs
// 例: 任意のライフタイムのストリングを処理できるパーサー
trait Parser {
// 任意の入力ライフタイムで動作
fn parse<'a>(&self, input: &'a str) -> Result<&'a str, &'a str>;
}
// パーサーを受け取る関数
fn use_parser<P>(parser: &P, input: &str) -> Result<&str, &str>
where
P: Parser,
{
parser.parse(input)
}
// トレイトオブジェクト化
struct IdentityParser;
impl Parser for IdentityParser {
fn parse<'a>(&self, input: &'a str) -> Result<&'a str, &'a str> {
Ok(input)
}
}
fn main() {
let parser = IdentityParser;
let input = String::from("hello");
match use_parser(&parser, &input) {
Ok(result) => println!("Parsed: {}", result),
Err(e) => println!("Error: {}", e),
}
}
4.3 標準ライブラリの例: Fn トレイト
// Fn トレイトの実際の定義(簡略版)
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
// 実際には、以下のように使われる
fn apply_to_3<F>(f: F) -> i32
where
F: Fn(i32) -> i32, // 実は F: for<'a> Fn(&'a i32) のような扱い
{
f(3)
}
// 複雑な例: 参照を取るクロージャ
fn map_ref<F>(f: F, x: &str) -> usize
where
F: for<'a> Fn(&'a str) -> usize,
{
f(x)
}
fn main() {
let length = map_ref(|s| s.len(), "hello");
println!("Length: {}", length);
}
4.4 実践的なケーススタディ
// ケース1: イテレータの変換
struct MapRef<I, F> {
iter: I,
func: F,
}
impl<'a, I, F, T, U> Iterator for MapRef<I, F>
where
I: Iterator<Item = &'a T>,
F: for<'b> Fn(&'b T) -> &'b U,
T: 'a,
U: 'a,
{
type Item = &'a U;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|x| (self.func)(x))
}
}
// ケース2: 非同期トレイト(概念的)
trait AsyncFn {
// 任意のライフタイムで Future を返す
fn call<'a>(&'a self) -> impl Future<Output = ()> + 'a;
}
---
5. ジェネリック型におけるライフタイムの変性
5.1 変性の実践的理解
// Covariant(共変)の例: Vec<T>
fn covariant_vec() {
let v: Vec<&'static str> = vec!["hello"];
// Vec<&'static str> は Vec<&'a str> として使える('a は 'static より短い)
fn print_vec<'a>(v: Vec<&'a str>) {
for s in v {
println!("{}", s);
}
}
print_vec(v); // OK: Vec は T について covariant
}
// Invariant(不変)の例: Cell<T>
use std::cell::Cell;
fn invariant_cell() {
let c: Cell<&'static str> = Cell::new("hello");
// エラー: Cell<&'static str> は Cell<&'a str> として使えない
// fn use_cell<'a>(c: Cell<&'a str>) { }
// use_cell(c); // エラー!Cell は T について invariant
}
5.2 PhantomData と変性の制御
use std::marker::PhantomData;
// PhantomData を使って変性を制御
struct MyType<'a, T>
where
T: 'a,
{
// PhantomData<&'a T>: 'a と T について covariant
_marker: PhantomData<&'a T>,
ptr: *const T, // 生ポインタは実際には使わない
}
// 実例: 独自のスマートポインタ
struct MyBox<T> {
ptr: *mut T,
_marker: PhantomData<T>, // T について covariant & owned
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
let ptr = Box::into_raw(Box::new(value));
MyBox {
ptr,
_marker: PhantomData,
}
}
fn get(&self) -> &T {
unsafe { &*self.ptr }
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
let _ = Box::from_raw(self.ptr);
}
}
}
5.3 変性の表(拡張版)
| 型 | T の変性 | 'a の変性 | 理由 |
|---|---|---|---|
| `&'a T` | Covariant | Covariant | 読み取り専用、安全に縮められる |
| `&'a mut T` | Covariant | Invariant | 書き込み可能、ライフタイムは固定 |
| `*const T` | Covariant | - | 生ポインタ、読み取り専用 |
| `*mut T` | Invariant | - | 生ポインタ、書き込み可能 |
| `fn(T) -> U` | Contravariant (T), Covariant (U) | - | 引数は反変、戻り値は共変 |
| `Cell |
Invariant | - | 内部可変性、サブタイピング不可 |
| `UnsafeCell |
Invariant | - | 全ての内部可変性の基礎 |
| `PhantomData |
Covariant | - | T を所有するかのように振る舞う |
| `PhantomData<&'a T>` | Covariant | Covariant | 不変参照のように振る舞う |
| `PhantomData<&'a mut T>` | Covariant | Invariant | 可変参照のように振る舞う |
---
6. Rust標準ライブラリのライフタイムパターン
6.1 Option と Result
// Option の定義(簡略版)
pub enum Option<T> {
Some(T),
None,
}
impl<T> Option<T> {
// as_ref: &Option<T> → Option<&T>
pub fn as_ref(&self) -> Option<&T> {
match self {
Some(x) => Some(x),
None => None,
}
}
// map: ライフタイムの変換
pub fn map<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(T) -> U,
{
match self {
Some(x) => Some(f(x)),
None => None,
}
}
}
// 使用例
fn main() {
let s = Some(String::from("hello"));
let r: Option<&str> = s.as_ref().map(|s| s.as_str());
// s: Option<String>
// s.as_ref(): Option<&String>
// .map(|s| s.as_str()): Option<&str>
}
6.2 Iterator
// Iterator の map 実装(簡略版)
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// map: ライフタイムを保持
fn map<B, F>(self, f: F) -> Map<Self, F>
where
Self: Sized,
F: FnMut(Self::Item) -> B,
{
Map::new(self, f)
}
}
// filter_map: ライフタイムの複雑な例
impl<I: Iterator> Iterator for I {
fn filter_map<B, F>(self, f: F) -> FilterMap<Self, F>
where
Self: Sized,
F: FnMut(Self::Item) -> Option<B>,
{
FilterMap::new(self, f)
}
}
// 実例: 文字列のイテレータ
fn main() {
let s = String::from("hello world");
let words: Vec<&str> = s.split_whitespace().collect();
// split_whitespace() → SplitWhitespace<'_>
// イテレータのライフタイムは s のライフタイムと同じ
}
6.3 Cow (Clone-on-Write)
use std::borrow::Cow;
// Cow の定義(簡略版)
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
impl<'a> Cow<'a, str> {
// 借用から所有への変換
pub fn into_owned(self) -> String {
match self {
Cow::Borrowed(s) => s.to_owned(),
Cow::Owned(s) => s,
}
}
// 可変参照を取得(必要に応じてクローン)
pub fn to_mut(&mut self) -> &mut String {
match self {
Cow::Borrowed(s) => {
*self = Cow::Owned(s.to_owned());
match self {
Cow::Owned(s) => s,
_ => unreachable!(),
}
}
Cow::Owned(s) => s,
}
}
}
// 実例
fn process_text(input: &str) -> Cow<str> {
if input.contains("foo") {
// 変更が必要: 所有型を返す
Cow::Owned(input.replace("foo", "bar"))
} else {
// 変更不要: 借用のまま
Cow::Borrowed(input)
}
}
fn main() {
let text = "hello foo world";
let result = process_text(text);
println!("{}", result); // "hello bar world"
}
---
7. プロダクションコードのケーススタディ
7.1 Kubernetes client-go (Rust移植版)
// Kubernetes の API レスポンス処理
// 実際のコードの簡略版
struct ApiResponse<'a> {
// JSON の生データへの参照
raw: &'a str,
// パースされたメタデータ
metadata: Metadata<'a>,
}
struct Metadata<'a> {
name: &'a str,
namespace: &'a str,
labels: HashMap<&'a str, &'a str>,
}
impl<'a> ApiResponse<'a> {
fn parse(raw: &'a str) -> Result<Self, Error> {
// raw を解析してメタデータを抽出
// 全てのフィールドが raw へのスライスなので、
// コピー不要(ゼロコピー解析)
let metadata = Metadata {
name: extract_name(raw)?,
namespace: extract_namespace(raw)?,
labels: extract_labels(raw)?,
};
Ok(ApiResponse { raw, metadata })
}
fn get_name(&self) -> &'a str {
// 'a のライフタイムを保持
self.metadata.name
}
}
// 使用例
fn main() {
let json = r#"{"metadata": {"name": "pod-1", "namespace": "default"}}"#;
let response = ApiResponse::parse(json).unwrap();
println!("Name: {}", response.get_name());
// json が生きている限り response も有効
}
7.2 tokio の非同期ランタイム
// tokio の JoinHandle 簡略版
use std::future::Future;
use std::pin::Pin;
pub struct JoinHandle<T> {
// タスクの結果を持つ Future
// 'static: タスクは独立して実行される
inner: Pin<Box<dyn Future<Output = T> + Send + 'static>>,
}
// タスクをスポーン
pub fn spawn<T>(future: T) -> JoinHandle<T::Output>
where
T: Future + Send + 'static,
T::Output: Send + 'static,
{
// future は 'static でないといけない
// 理由: タスクは別スレッドで実行される可能性がある
JoinHandle {
inner: Box::pin(future),
}
}
// 使用例
async fn async_task() {
// OK: 参照を含まない
let handle = spawn(async {
42
});
// エラー: 参照を含む
let x = 42;
// let handle = spawn(async {
// &x // エラー!'static ではない
// });
}
7.3 serde の Deserialize
// serde の Deserialize トレイト(簡略版)
pub trait Deserialize<'de>: Sized {
// 'de: デシリアライズ元のデータのライフタイム
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
}
// ゼロコピーデシリアライズの例
#[derive(Deserialize)]
struct Config<'a> {
// 'a: 入力データへの参照
#[serde(borrow)]
name: &'a str,
#[serde(borrow)]
value: &'a str,
}
// 使用例
fn main() {
let json = r#"{"name": "key", "value": "val"}"#;
// Config のフィールドは json のスライスを指す
let config: Config = serde_json::from_str(json).unwrap();
println!("{}: {}", config.name, config.value);
// json が生きている限り config も有効
}
---
まとめ
この章で学んだこと
'a: 'b)
- 最小共通ライフタイムの計算- ライフタイム省略規則:
'_ の使い方- ライフタイム境界:
T: 'a の意味
- T: 'static の実用例
- 複雑な境界の組み合わせ- HRTBs:
for<'a> の実践的使用
- クロージャとトレイトオブジェクト
- 標準ライブラリのパターン- 変性:
PhantomData による変性制御
- 安全性との関係- 実世界の例:
次の章へ
次の章では、自己参照構造、Pin とUnpin、そして安全でないライフタイム操作を学びます。
---
自己チェック問題
問題1: 複数ライフタイム
以下のコードが正しいか説明しなさい:fn mix<'a, 'b>(x: &'a str, y: &'b str) -> (&'a str, &'a str)
where
'b: 'a,
{
(x, y)
}
問題2: 省略規則
以下の関数にライフタイム注釈を補完しなさい:fn parse(input: &str) -> Result<&str, &str> {
// ...
}
問題3: HRTBs
なぜ以下のコードは正しいか説明しなさい:fn apply<F>(f: F) -> i32
where
F: for<'a> Fn(&'a i32) -> &'a i32,
{
f(&42)
}
問題4: 変性
なぜCell は invariant か説明しなさい。問題5: 標準ライブラリ
Option::as_ref がどのようにライフタイムを変換するか説明しなさい。---
参考文献
- The Rust Reference - Lifetime Elision. https://doc.rust-lang.org/reference/lifetime-elision.html
- Rustonomicon - Lifetimes. https://doc.rust-lang.org/nomicon/lifetimes.html
- Rust RFC 0599 - Default object lifetime bounds. https://rust-lang.github.io/rfcs/0599-default-object-bound.html
- Rust RFC 1214 - Clarify (and improve) rules for projections and well-formedness. https://rust-lang.github.io/rfcs/1214-projections-lifetimes-and-wf.html
- tokio documentation. https://docs.rs/tokio/
- serde documentation. https://docs.rs/serde/
- Kubernetes client-go. https://github.com/kubernetes/client-go