📚 背景知識(読んでから問題へ)
Day 017・018ではIterator・DoubleEndedIterator・ExactSizeIteratorを自作し、next()を1つ実装するだけで.map()・.filter()・.rev()・.collect()のような70個以上のメソッドが無料で手に入ることを体験しました。今日はその「なぜ無料で手に入るのか」というメカニズムそのものと、Iterator契約の中でも見落とされやすい2つの仕様――size_hint()の契約範囲と「消費し尽くした後」の仕様の曖昧さ(FusedIterator)――を掘り下げます。
pub trait Iterator {
type Item;
// 唯一の必須メソッド(required method)
fn next(&mut self) -> Option<Self::Item>;
// 以下はすべて「デフォルト実装」を持つメソッド(provided methods)
// next() の呼び出し列だけを使って書かれている
fn size_hint(&self) -> (usize, Option<usize>) { (0, None) }
fn count(self) -> usize where Self: Sized { /* next()をNoneまで呼んで数える */ }
fn map<B, F>(self, f: F) -> Map<Self, F> where Self: Sized, F: FnMut(Self::Item) -> B { /* ... */ }
fn filter<P>(self, predicate: P) -> Filter<Self, P> where Self: Sized, P: FnMut(&Self::Item) -> bool { /* ... */ }
fn fold<B, F>(self, init: B, f: F) -> B where Self: Sized, F: FnMut(B, Self::Item) -> B { /* ... */ }
fn collect<B: FromIterator<Self::Item>>(self) -> B where Self: Sized { /* ... */ }
// ...他60個以上
}
mapやfilterのようなメソッドの本体は、Rust標準ライブラリのソース内で完全に書き切られています。あなたが自分の型に対して書くのはnext()の実装だけであり、それ以外のメソッドは「あなたのnext()を呼び出す汎用コード」としてトレイトの中にすでに存在しています。これはimplブロックでオーバーライドしない限りそのまま使われる、いわゆるデフォルトメソッド(provided methods)という仕組みです。
さらに重要なのは、これらのメソッドが実行時に呼ばれる際、Rustは単相化(monomorphization)によって、あなたの具体的な型専用のコードをコンパイル時に生成します。my_iter.map(f).filter(g).sum::<i32>()のようなメソッド連鎖は、実行時に「次は誰のnext()を呼ぶか」を毎回問い合わせる動的ディスパッチ(vtable経由の呼び出し)ではなく、コンパイル時にすべてインライン化された1つのループへと最適化されます。これが「ゼロコスト抽象化」の具体例であり、C++のテンプレートやGoのジェネリクス(型消去ベース)との大きな違いです。
📝 問題
以下の3つの問いに答えてください。いずれもコードを書く前に、まず言葉で説明することが主目的です(要求3のみ、説明の正しさを検証するための実装を伴います)。
要求1(メカニズムの説明)
上記のmapやfoldのようなデフォルトメソッドが、あなたが定義したnext()だけを使ってどうやって動作するのかを、トレイトのデフォルトメソッドという言語機能の観点から説明してください。また、これらのメソッド呼び出しが実行時にvtable経由の動的ディスパッチにならず、コンパイル時にインライン化される理由(単相化)を、Javaのインターフェースのデフォルトメソッド(defaultキーワード)との違いに触れながら説明してください。
要求2(size_hint()の契約範囲の説明)
size_hint()のシグネチャはfn size_hint(&self) -> (usize, Option<usize>)で、戻り値は(下限, 上限)を表します。標準ライブラリのドキュメントには次のように書かれています。
A default implementation is provided ... it is not enforced that an iterator implementation yields the declared number of elements. Cheating is allowed, but consumers relying on the values to be correct should not do so if the iterator implementation yields incorrect values, e.g. by yielding fewer elements than the lower bound indicates.
(出典:std::iter::Iterator::size_hintのドキュメント要約)
つまり、size_hint()が実際の要素数と食い違う値を返してもコンパイルエラーにもパニックにもならず、unsafeでもありません。しかしDay 018で扱ったExactSizeIterator::len()はsize_hint()の下限をベースにしつつ、より強い「正確性の表明」として使われます。Vec<T>::with_capacityを使うcollect::<Vec<T>>()がsize_hint()の値を信じて事前確保を行うとき、実際の要素数がその値を上回った場合と下回った場合とで、それぞれ何が起きるか(パニックか、単なる非効率か、それとも未定義動作か)を説明してください。そのうえで、なぜRustの設計者は「size_hint()が嘘をついてもメモリ安全性は破られない」という制約のもとでこの仕組みを設計したのかを述べてください。
要求3(FusedIteratorと検証実装)
Iteratorの公式な契約では、「一度next()がNoneを返した後、再びnext()を呼んだときに何を返すべきか」は規定されていません。多くのイテレータは一度Noneを返したら以後もずっとNoneを返す(これを「fusedである」と呼ぶ)よう実装されていますが、これは慣習であって言語仕様の強制ではありません。
次のAlternatorという非fusedなイテレータを実装し、.next()を5回呼び出した結果が[Some(1), None, Some(1), None, Some(1)]というNoneを挟んで値が復活するパターンになることを実際にテストで示してください。そのうえで、標準アダプタ.fuse()でラップすると、2回目以降はずっとNoneのままになる([Some(1), None, None, None, None])ことも同じテスト関数内で確認してください。
struct Alternator {
calls: u32,
}
impl Iterator for Alternator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.calls += 1;
// 奇数回目の呼び出しでは Some(1)、偶数回目では None を返す
// (TODO: この仕様に合わせて実装する)
todo!()
}
}
.fuse()がなぜ必要とされるのか(FusedIteratorマーカートレイトが存在する理由を含めて)を、実装とテストの結果から説明してください。
🔍 ヒント(段階的開示)
ヒント1 — 方向性
- 要求1:
implブロックでnext()だけを書いたとき、コンパイラは「このトレイトの他のメソッドは呼ばれていないから生成しなくていい」とは判断しません。むしろジェネリック関数と同じ扱いで、呼ばれた箇所ごとに具体的な型のためのコードが生成される、という単相化の仕組みを思い出してください - 要求2: 「メモリ安全性」と「ロジックの正しさ」は別レイヤーです。
size_hint()はメモリ安全性に関わるAPI設計上、意図的に「信頼できなくても壊れない」よう設計されています。一方でunsafeなTrustedLenという別のマーカートレイトが存在することにも注目してください - 要求3:
self.callsが奇数か偶数かで分岐するif文を1つ書くだけで実装できます。.fuse()は標準ライブラリのstd::iter::Fuseという薄いラッパー型で、内部に「一度Noneが出たかどうか」のフラグを持っています
ヒント2 — アプローチ
- 要求1:
map・filterなどのデフォルト実装は、あなたのnext()を呼ぶ「アダプタ構造体」(Map<Self, F>のような型)を返します。このアダプタ自身もIteratorを実装しており、そのアダプタのnext()の中で内側のイテレータのnext()を呼び出す、という入れ子構造になっています。この入れ子はすべて具体的な型として展開されるため、sum()やcollect()のように最終的に消費するメソッドまで辿ると、コンパイラは「型Aのnext()→型Bのnext()→型Cのnext()」という呼び出し列全体を1つの関数としてインライン展開できます。Javaのdefaultメソッドは実行時にインターフェース経由(多くは動的ディスパッチ)で解決されるため、このような連鎖全体のインライン化は保証されません - 要求2:
size_hint()が実際の要素数より多い上限を返した場合、Vec::with_capacityが必要以上に大きい容量を確保するだけで、安全性の問題は起きません(無駄なメモリ確保という非効率で済みます)。実際の要素数より少ない下限を返した場合も、collect()は容量が足りなくなった時点で普通に再アロケーションを行うため、こちらも安全性の問題にはなりません。size_hint()を信じてunsafeに「この件数ぶんだけメモリを確保して境界チェックなしで書き込む」という最適化をしてしまうと初めて未定義動作になり得るため、標準ライブラリはそのような最適化をsize_hint()単体には許さず、より強い契約を持つunsafe trait TrustedLenという別のマーカートレイトを用意しています - 要求3:
Fuse<I>は概ね次のような構造を持ちます。struct Fuse<I> { iter: Option<I> }。next()が呼ばれるたびにself.iterがSomeならラップされたイテレータのnext()を呼び、それがNoneを返したらself.iterをNoneに置き換えてしまいます。以後は内側のイテレータを二度と呼ばず、無条件にNoneを返します
ヒント3 — コード骨格
impl Iterator for Alternator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.calls += 1;
if self.calls % 2 == 1 {
Some(1)
} else {
None
}
}
}
#[test]
fn alternator_and_fuse_behavior() {
let mut raw = Alternator { calls: 0 };
let raw_results: Vec<Option<i32>> = (0..5).map(|_| raw.next()).collect();
assert_eq!(raw_results, vec![Some(1), None, Some(1), None, Some(1)]);
let mut fused = Alternator { calls: 0 }.fuse();
let fused_results: Vec<Option<i32>> = (0..5).map(|_| fused.next()).collect();
assert_eq!(fused_results, vec![Some(1), None, None, None, None]);
}
✅ 模範解答
// ---- 要求3: Alternator の実装と .fuse() の検証 ----
struct Alternator {
calls: u32,
}
impl Iterator for Alternator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.calls += 1;
// 奇数回目の呼び出しでは Some(1)、偶数回目では None を返す非fusedなイテレータ
if self.calls % 2 == 1 {
Some(1)
} else {
None
}
}
}
// ---- 要求1の検証用: next() のみで map/filter/fold/sum が使えることを示す ----
struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current >= self.max {
return None;
}
self.current += 1;
Some(self.current)
}
}
fn main() {
// next() しか実装していない Counter で map/filter/sum が動く
let total: u32 = Counter::new(5)
.map(|x| x * 2) // デフォルトメソッド(next()の呼び出し列だけで書かれている)
.filter(|x| x % 4 != 0)
.sum(); // 消費アダプタもデフォルトメソッド
println!("total = {total}"); // (2,4,6,8,10) -> 4 は除外 -> 2+6+8+10 = 26
// Alternator: 非fusedな挙動をそのまま観測する
let mut raw = Alternator { calls: 0 };
println!("raw: {:?}", (0..5).map(|_| raw.next()).collect::<Vec<_>>());
// .fuse() でラップすると一度 None が出た後は常に None になる
let mut fused = Alternator { calls: 0 }.fuse();
println!("fused: {:?}", (0..5).map(|_| fused.next()).collect::<Vec<_>>());
}
#[cfg(test)]
mod tests {
use super::*;
// 要求1の検証: next() のみの実装から map/filter/fold/sum がそのまま使える
#[test]
fn default_methods_work_from_next_only() {
let total: u32 = Counter::new(5).map(|x| x * 2).filter(|x| x % 4 != 0).sum();
assert_eq!(total, 26);
let folded = Counter::new(4).fold(0, |acc, x| acc + x);
assert_eq!(folded, 1 + 2 + 3 + 4);
let collected: Vec<u32> = Counter::new(3).collect();
assert_eq!(collected, vec![1, 2, 3]);
}
// 要求3: Alternator は非fused、.fuse() でラップすると fused になる
#[test]
fn alternator_and_fuse_behavior() {
let mut raw = Alternator { calls: 0 };
let raw_results: Vec<Option<i32>> = (0..5).map(|_| raw.next()).collect();
assert_eq!(raw_results, vec![Some(1), None, Some(1), None, Some(1)]);
let mut fused = Alternator { calls: 0 }.fuse();
let fused_results: Vec<Option<i32>> = (0..5).map(|_| fused.next()).collect();
assert_eq!(fused_results, vec![Some(1), None, None, None, None]);
}
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
total = 26
raw: [Some(1), None, Some(1), None, Some(1)]
fused: [Some(1), None, None, None, None]
$ cargo test
running 2 tests
test tests::alternator_and_fuse_behavior ... ok
test tests::default_methods_work_from_next_only ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
🪜 Step-by-Step 解説
Iteratorトレイトのmap・filter・fold・sumは、トレイト定義の内部にすでにコードが書かれている「provided methods」です。あなたがimpl Iterator for Counter { fn next(...) {...} }と書いた瞬間、コンパイラはCounterという具体的な型に対してIteratorトレイト全体(next()はあなたの実装、それ以外は標準ライブラリの実装)を「実体化」します。Counter::new(5).map(f)が呼ばれると、コンパイラはmapの型パラメータSelfをCounter、Fをクロージャの具体的な型に固定した専用コードを生成します(これが単相化=monomorphization)。.filter(g).sum()とさらにメソッドを連鎖させると、それぞれのアダプタ(Map<Counter, F>、Filter<Map<Counter, F>, G>のような入れ子の具体型)についても同様に専用コードが生成され、最終的にはコンパイラの最適化パスによって1本のループにインライン化されます。実行時には「次にどの型のnext()を呼ぶか」を問い合わせる分岐は一切残らず、手書きのforループと同じ機械語になります。
Javaのinterfaceにおけるdefaultメソッドは概念としては似ていますが、Javaの多態性は基本的に実行時の仮想メソッドテーブル(vtable)経由で解決されます。JITコンパイラがインライン化することもありますが、それは実行時プロファイルに基づく最適化であり、Rustのようにコンパイル時に必ず具体型へ展開されるという保証はありません。この「コンパイル時に確実に消える抽象化コスト」がRustの言うゼロコスト抽象化の実体です。
size_hint()は「信頼してもよいが、破られても安全」size_hint()の戻り値(下限, 上限)は、collect::<Vec<T>>()が内部でVec::with_capacity(下限)を呼ぶ根拠として使われます。
- 実際の要素数が下限より多い場合: 容量が足りなくなった時点で
Vecは通常の再アロケーション(現在の容量を伸長して新しいメモリ確保+コピー)を行うだけです。安全性の問題はなく、単に「事前確保が効かず、余計なコピーが1回以上発生する」という性能上の損にとどまります - 実際の要素数が上限や下限より少ない場合: 単に
Vecの容量に余りが出るだけで、これも安全性の問題にはなりません。無駄なメモリを確保したという非効率で済みます
つまりsize_hint()がどんな嘘の値を返しても、メモリ破壊やUB(未定義動作)には至りません。これはRustの設計として意図的なものです。Vec::with_capacityのような「事前確保」の最適化は、あくまでVec自身が持つ境界チェック付きのpushによって安全性が担保された状態で行われるため、size_hint()の値そのものはヒントに過ぎず信頼の起点にはなりえないという前提で設計されています。もし本当に「要素数を寸分違わず信じてよい」という強い契約が必要な最適化(例えば境界チェックなしで直接メモリに書き込む)をしたい場合は、size_hint()ではなくunsafe trait TrustedLen(安全な実装者が正確性を保証する責任を負う、より強い契約のマーカートレイト)を使う設計になっています。「弱い契約は安全側に倒し、強い契約が必要な場面はunsafeという形で責任の所在を明示する」という段階的な信頼モデルが、この設計の根底にあります。
FusedIteratorが存在する理由Alternatorの実装は次の状態遷移を持ちます。
| 呼び出し回数 | self.calls | self.calls % 2 | 戻り値 |
|---|---|---|---|
| 1回目 | 1 | 奇数 | Some(1) |
| 2回目 | 2 | 偶数 | None |
| 3回目 | 3 | 奇数 | Some(1)(Noneの後に値が復活) |
| 4回目 | 4 | 偶数 | None |
| 5回目 | 5 | 奇数 | Some(1) |
Iteratorの公式な契約は「next()が一度Noneを返した後の挙動」を規定していないため、このAlternatorは言語仕様に違反していませんが、多くのコード(特にforループの脱糖後のコードや、一部の手書きアダプタ)は「一度Noneが出たらそれ以降はNoneしか来ない」という暗黙の前提で書かれがちです。.fuse()でラップすると、内部のFuse<Alternator>は最初にNoneを観測した時点で内側のAlternatorへの参照を手放し(あるいは「終了済み」フラグを立て)、以後Alternator::next()を二度と呼ばずに無条件でNoneを返すようになります。これにより「本来なら値が復活するはずのイテレータ」を安全に「fusedな」ものへ変換できます。FusedIteratorマーカートレイトは、「このイテレータは自前ですでにfusedな実装になっている」ことを型で表明し、.fuse()でラップしたときに余計なフラグ管理のオーバーヘッドすら省略できるようにするための最適化フックです(Counterのように自明にfusedな実装であればimpl FusedIterator for Counter {}を追加でき、Fuse<Counter>はラップのオーバーヘッドなしにCounterとほぼ同じコードに最適化されます)。
💡 設計思想・なぜこう書くのか
Iterator(弱い契約: next()だけあればよく、契約を破っても安全性は壊れない)→ExactSizeIterator(やや強い契約: len()は正確であることが期待される)→TrustedLen(unsafeな最強の契約: 正確性を実装者が保証し、それを前提に境界チェックなしの最適化が許可される)という段階的なトレイト設計を各所で採用しています。これは「安全性はデフォルトで担保しつつ、より強い最適化がほしい人だけがより強い責任(unsafe)を引き受ける」というRust全体の設計哲学の縮図です。Iteratorが「None後の挙動」を規定していないのは手抜きではなく、意図的な設計です。すべての具体的な挙動を仕様化してしまうと、将来「一度Noneを返した後にリソースを再利用して別の値を返す」ような特殊なイテレータ(例えばラウンドロビン的な生成器)を実装する余地がなくなってしまいます。「規定しないことで自由度を残しつつ、多くの利用者が期待する挙動(fused)を得たい場合は.fuse()という明示的なオプトインを用意する」というのは、C言語のような暗黙のUBに頼る設計とは対照的な、規定しないことを明示するRust流のアプローチです。🌐 他言語との比較
| 観点 | Rust | Java | Go | JavaScript/TypeScript | C++ |
|---|---|---|---|---|---|
| デフォルトメソッドの解決タイミング | コンパイル時に単相化され、多くの場合インライン化される(ゼロコスト) | interfaceのdefaultメソッドは実行時にvtable経由で解決されるのが基本(JITでインライン化されることもあるが保証はない) | インターフェースの概念自体が薄く、メソッドの共通実装はコード生成や埋め込み(embedding)で代替する文化 | プロトタイプチェーン経由での実行時解決。ジェネレータ関数(function*)による遅延評価は言語機能として存在 | テンプレートによる静的ポリモーフィズムに近く、コンパイル時にインスタンス化される点はRustと似る。ただしstd::rangesのview合成もほぼゼロコストを志向 |
| 「消費し尽くした後」の仕様 | 未規定(FusedIteratorでopt-inすると保証される) | Iterator.hasNext()/next()は多くの実装で「尽きたら常にfalse/例外」が慣習だが、これも言語仕様として強制はされない | rangeの対象(スライス・チャネル・イテレータ関数)は尽きたらループが終わるだけで、「尽きた後に再度呼ぶ」操作自体が構文上あまり露出しない | ジェネレータは一度完了(done: true)すると、それ以降は仕様上常にdone: trueを返すことがECMAScript仕様で明確に規定されている | end()イテレータに到達した後の++は未定義動作(UB)になりうる範囲が多く、Rustよりも安全側の保証が弱い |
| 事前サイズ情報の扱い | size_hint()は「嘘をついても安全」なヒント、ExactSizeIterator・TrustedLenは段階的に強い契約 | Collection.size()は正確な値を返す契約(コレクションベースのため自明に得られる) | len()組み込み関数はスライス・マップに対して正確な値を返すのが前提で、「嘘のヒント」という概念自体が薄い | 配列は.lengthで正確な値が取れるが、ジェネレータには事前サイズの概念がなく、必要なら都度.toArray()相当で実体化するしかない | std::distanceやstd::ranges::sizeはイテレータの種類によってO(1)またはO(n)になり、事前ヒントより実測に近い設計 |
JavaScriptのジェネレータが「完了後は常にdone: true」という挙動を言語仕様として保証しているのに対し、Rustが同じ挙動をFusedIteratorというopt-inのマーカートレイトとして切り出しているのは対照的です。これはRustが「保証を強制する」よりも「保証の有無を型で区別可能にし、必要な保証だけを選び取れるようにする」ことを好む言語だからです。ECMAScriptのように全ジェネレータに一律で強い保証を課すと、その保証を実現するための状態管理コスト(内部フラグ等)がすべての実装に強制されますが、Rustでは自明にfusedな実装(Counterのような単純な数値カウンタ)はそのコストを払わずに済み、必要な場合だけ.fuse()で明示的にコストを払う、という選択制になっています。
🏆 実務での使いどころ
- ページネーションAPIクライアントのイテレータ化: 「次ページがなくなったらもう二度と呼ばれない」という前提でHTTPリクエストの発行を止めるコードを書く場合、対象のイテレータが
FusedIteratorかどうかを型境界で要求しておくと、「尽きた後にもう一度呼ばれてリクエストが飛んでしまう」バグを未然に防げます collect()のパフォーマンスチューニング: 大量データをパースしてVecに集約するホットパスでは、size_hint()を正確に実装するかどうかが再アロケーション回数に直結します。CSVパーサーやログパーサーで「既知の行数」がある場合にExactSizeIteratorまで実装しておくと、collect()のメモリ確保が1回で済みます- 無限イテレータと
.take()の組み合わせ:size_hint()のデフォルト実装(0, None)(上限不明)は、無限イテレータ(乱数生成器・カウンタ等)が自分の意思で「要素数は分からない」と正直に申告するための仕組みです。これを(固定値, Some(固定値))のように偽って実装してしまうと、collect()が誤った容量でVecを確保し、後続の再アロケーションで無駄なコピーが発生します
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
size_hint()が間違った値を返すとunsafeと同様にメモリ破壊が起きると思い込む | unsafeブロックへの警戒心から、契約違反はすべて危険だと一般化してしまう | size_hint()単体の契約違反は安全(性能劣化にとどまる)。危険なのはsize_hint()の値を元に境界チェックなしでメモリへ直接書き込むような、別のunsafeコードと組み合わせたときだけ |
すべてのイテレータは自動的にfusedだと思い込み、.fuse()は何もしない冗長な呼び出しだと考えてしまう | 実務で出会うイテレータの大半(Vec::iter()・Range等)が実際にfusedな挙動をするため、それが言語保証だと錯覚する | fusedであることは慣習であり言語仕様ではない。標準コレクションのイテレータがfusedなのは「そう実装されているから」であって、自作イテレータが同じ性質を持つとは限らない |
mapやfilterのようなデフォルトメソッドは「トレイトオブジェクト(dyn Iterator)を使わない限り常に無コスト」だと思い込む | 静的ディスパッチが基本という説明を過度に一般化してしまう | Box<dyn Iterator<Item = T>>のようにトレイトオブジェクトとして扱うと、動的ディスパッチのコストが発生しインライン化の恩恵も失われる。ゼロコストが成立するのはジェネリクス経由の静的ディスパッチのときに限られる |
🚀 次のステップ
- 発展:
Counterにimpl FusedIterator for Counter {}(マーカートレイトの空実装)を追加し、Counter::new(3).fuse()が余計なラップ構造体を持たずに元のCounterとほぼ同じコードへ最適化されることを、標準ライブラリのFuse<I>のドキュメント(I: FusedIteratorのときはFuse<I> = I相当に特殊化される旨の記述)で確認してみてください - 次回予告: 自己評価で「完全理解」または「おおむね理解」が3回連続で記録された時点で、次のテーマスマートポインタ①(Box<T>)(ヒープ確保・再帰的データ構造)に進みます