📚 背景知識(読んでから問題へ)
Day 022のテストでこう書きました。
IterMutを2回forループに通せない(IntoIteratorではなく直接Iteratorを実装しているため2周目は空になる)」今日はこの「IntoIteratorではなく直接Iterator」という違いを正面から扱います。
for x in something { ... }は、実はただの糖衣構文(syntactic sugar)です。コンパイラはこれを次のように脱糖します。
let mut iter = IntoIterator::into_iter(something);
loop {
match Iterator::next(&mut iter) {
Some(x) => { /* ループ本体 */ }
None => break,
}
}
つまりforループが本当に呼んでいるのはIntoIterator::into_iter()であり、Iteratorはその後ろでnext()を呼ばれる側に過ぎません。IntoIteratorは「自分自身をイテレータに変換する方法」を定義するトレイトです。
pub trait IntoIterator {
type Item;
type IntoIter: Iterator- ;
fn into_iter(self) -> Self::IntoIter;
}
ここで重要なのは、into_iter(self)のselfが値渡しであるという点です。Vec<T>は次の3つの型に対してそれぞれ別のIntoIterator実装を持っています。
| 書き方 | 実際に呼ばれる実装 | Itemの型 | 意味 |
|---|---|---|---|
for x in vec | impl IntoIterator for Vec<T> | T(所有権ごと) | vecを消費し、各要素の所有権を1つずつ取り出す |
for x in &vec | impl<'a> IntoIterator for &'a Vec<T> | &'a T | vecを消費せず、不変参照で読むだけ |
for x in &mut vec | impl<'a> IntoIterator for &'a mut Vec<T> | &'a mut T | vecを消費せず、各要素を書き換える |
vec.into_iter() / vec.iter() / vec.iter_mut()という3つのメソッドが存在するのは偶然ではなく、まさにこの3つのIntoIterator実装(正確にはiter()/iter_mut()はIntoIteratorとは別の固有メソッドですが、&vec/&mut vecに対するIntoIterator実装は内部でこれらと同じイテレータ型を使います)に対応しています。今日は自作のStack<T>に、この3つの反復方式をすべて実装します。
📝 問題
以下のStack<T>(LIFO=後入れ先出しのスタック)に対して、3種類のIntoIteratorを実装してください。スタックは「上から下へ」(=最後にpushしたものから先に)反復されるものとします。
pub struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
pub fn new() -> Self {
Stack { items: Vec::new() }
}
pub fn push(&mut self, item: T) {
self.items.push(item);
}
pub fn pop(&mut self) -> Option<T> {
self.items.pop()
}
}
課題
impl IntoIterator for Stack<T>を実装してください。for x in stack(stack: Stack<T>)と書いたとき、x: Tとして、上から下へ(=pushした逆順に)所有権ごと1個ずつ取り出せるようにしてください。Stack<T>を消費しますimpl<'a> IntoIterator for &'a Stack<T>を実装してください。for x in &stackと書いたとき、x: &'a Tとして、上から下へ参照だけを1個ずつ取り出せるようにしてください。stackは消費されませんimpl<'a> IntoIterator for &'a mut Stack<T>を実装してください。for x in &mut stackと書いたとき、x: &'a mut Tとして、上から下へ各要素を書き換えられるようにしてください- いずれの実装も、自分でイテレータの状態を持つ構造体を1から書くのではなく、
Vec<T>が既に持っているIntoIter(std::vec::IntoIter<T>)・std::slice::Iter<'a, T>・std::slice::IterMut<'a, T>を内部で借りて.rev()をかけるだけで済ませてください(ゼロから車輪の再発明をしないという実務上の判断も評価対象です) mainで次の3パターンすべてを実際に動かし、それぞれの意味の違いをコメントで説明してくださいfor x in &stack { ... }(読むだけ、stackはループ後も使える)for x in &mut stack { *x += 100; }(書き換え、stackはループ後も使える)for x in stack { ... }(消費、ループ後はstackを使おうとするとコンパイルエラーになることを確認)
#[test]で「push(1); push(2); push(3)した後、&stackで反復すると[3, 2, 1]の順で得られること」を検証してください
🔍 ヒント(段階的開示)
ヒント1 — 方向性
Vec<T>::into_iter()は既にstd::vec::IntoIter<T>という「所有権ごと消費するイテレータ」を返します。これはDoubleEndedIteratorを実装しているので、.rev()が使えます- 「上から下へ」=「
Vecの末尾から先頭へ」なので、self.items.into_iter().rev()のように、既存のイテレータに.rev()を1回かけるだけで今日の課題の反復順序は満たせます。Day 023で学んだDoubleEndedIteratorがここで実際に活きてきます
ヒント2 — アプローチ
- 3つの
impl IntoIteratorは、それぞれtype IntoIterに別の具体型を指定する必要があります。自分で列挙すると次のようになりますStack<T>用:type IntoIter = std::iter::Rev<std::vec::IntoIter<T>>;&'a Stack<T>用:type IntoIter = std::iter::Rev<std::slice::Iter<'a, T>>;&'a mut Stack<T>用:type IntoIter = std::iter::Rev<std::slice::IterMut<'a, T>>;
std::iter::Rev<I>は.rev()を呼んだときに標準ライブラリが返す具体型です。型名を正確に書くのが面倒に感じる場合は、implトレイト構文(type IntoIter = impl Iterator<Item = T>;)は現状typeエイリアスの安定機能では制限があるため、今日は素直にstd::iter::Rev<...>を書き切る練習として扱ってください&'a Stack<T>のinto_iter(self)の中身はself.items.iter().rev()です(self: &'a Stack<T>なのでself.items: &'a Vec<T>として扱われ、.iter()が'aを持ったIter<'a, T>を返します)
ヒント3 — コード骨格
impl<T> IntoIterator for Stack<T> {
type Item = T;
type IntoIter = std::iter::Rev<std::vec::IntoIter<T>>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter().rev()
}
}
impl<'a, T> IntoIterator for &'a Stack<T> {
type Item = &'a T;
type IntoIter = std::iter::Rev<std::slice::Iter<'a, T>>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter().rev()
}
}
impl<'a, T> IntoIterator for &'a mut Stack<T> {
type Item = &'a mut T;
type IntoIter = std::iter::Rev<std::slice::IterMut<'a, T>>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter_mut().rev()
}
}
✅ 模範解答
// このコードは cargo new day024_stack_intoiter && cd day024_stack_intoiter の後、
// src/main.rs をこの内容で置き換えて `cargo run` / `cargo test` で検証できます。
pub struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
pub fn new() -> Self {
Stack { items: Vec::new() }
}
pub fn push(&mut self, item: T) {
self.items.push(item);
}
pub fn pop(&mut self) -> Option<T> {
self.items.pop()
}
}
// ① for x in stack -> x: T(所有権ごと消費)
impl<T> IntoIterator for Stack<T> {
type Item = T;
type IntoIter = std::iter::Rev<std::vec::IntoIter<T>>;
fn into_iter(self) -> Self::IntoIter {
// Vec::into_iter() は先頭(push した最初の要素)から順に返すので、
// 「上から下へ」(最後に push した要素が先)にするには .rev() が必要。
self.items.into_iter().rev()
}
}
// ② for x in &stack -> x: &T(不変参照、stackは消費されない)
impl<'a, T> IntoIterator for &'a Stack<T> {
type Item = &'a T;
type IntoIter = std::iter::Rev<std::slice::Iter<'a, T>>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter().rev()
}
}
// ③ for x in &mut stack -> x: &mut T(可変参照、stackは消費されない)
impl<'a, T> IntoIterator for &'a mut Stack<T> {
type Item = &'a mut T;
type IntoIter = std::iter::Rev<std::slice::IterMut<'a, T>>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter_mut().rev()
}
}
fn main() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
// パターン1: `&stack` -> 読むだけ。ループ後も stack をそのまま使い続けられる。
print!("読むだけ: ");
for x in &stack {
print!("{x} ");
}
println!();
// パターン2: `&mut stack` -> 各要素をその場で書き換える。stack は消費されない。
for x in &mut stack {
*x += 100;
}
print!("書き換え後: ");
for x in &stack {
print!("{x} ");
}
println!();
// パターン3: `stack`(値そのもの) -> Stack を消費し、各要素の所有権を取り出す。
// ループが終わった後は stack はムーブ済みなので、もう使えない
// (下のコメントアウトを外すと "value borrowed here after move" でコンパイルエラーになる)
print!("消費: ");
for x in stack {
print!("{x} ");
}
println!();
// println!("{}", stack.pop().is_some()); // ← コンパイルエラー: stack は上の for でムーブ済み
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn borrowing_iteration_yields_top_to_bottom_order() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
let collected: Vec<&i32> = (&stack).into_iter().collect();
assert_eq!(collected, vec![&3, &2, &1]);
// & で反復しても stack はまだ使える(消費されていない)ことの確認
assert_eq!(stack.pop(), Some(3));
}
#[test]
fn mutable_iteration_updates_in_place_without_consuming() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
for x in &mut stack {
*x *= 10;
}
let collected: Vec<&i32> = (&stack).into_iter().collect();
assert_eq!(collected, vec![&20, &10]);
}
#[test]
fn owned_iteration_consumes_the_stack() {
let mut stack = Stack::new();
stack.push('a');
stack.push('b');
stack.push('c');
let collected: Vec<char> = stack.into_iter().collect();
assert_eq!(collected, vec!['c', 'b', 'a']);
// ここで stack を使おうとするとコンパイルエラーになる(上の for/collect でムーブ済み)
}
}
▶ 実行結果を見る(cargo run / cargo test・実機で検証済み)
$ cargo run
読むだけ: 3 2 1
書き換え後: 103 102 101
消費: 103 102 101
$ cargo test
running 3 tests
test tests::borrowing_iteration_yields_top_to_bottom_order ... ok
test tests::mutable_iteration_updates_in_place_without_consuming ... ok
test tests::owned_iteration_consumes_the_stack ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
🪜 Step-by-Step 解説
for x in exprは3通りに脱糖されるforループが呼ぶのはあくまでIntoIterator::into_iter(self)であり、selfの型(Stack<T>そのものか、&Stack<T>か、&mut Stack<T>か)によってどのimplブロックが選ばれるかが変わります。これは関数オーバーロードではなく、普通のトレイト実装の選択です。for x in &stackと書いた時点で、コンパイラは「&Stack<T>型の値に対してIntoIterator::into_iterを呼ぶ」と解釈し、impl<'a, T> IntoIterator for &'a Stack<T>を選びます。
今日の実装は、
Iteratorトレイトのnext()を1から書いていません。Vec<T>が既に持っているinto_iter() / iter() / iter_mut()の戻り値に.rev()を1回かけているだけです。これは「車輪の再発明をしない」という実務判断そのものです。std::vec::IntoIter<T>・std::slice::Iter<'a, T>・std::slice::IterMut<'a, T>はいずれも標準ライブラリの中で高度に最適化された実装(境界チェックの削減など)を持っており、自作のnext()よりも確実に速く、確実にバグが少ないです。
type IntoIterに具体型を書く理由IntoIterator::into_iterの戻り値の型はSelf::IntoIterという関連型で指定する必要があります。今日のように.rev()を使う場合、戻ってくる型はstd::iter::Rev<元のイテレータ型>という入れ子になった具体型になります。これを正確に書けることは、「今書いているメソッドチェーンが最終的にどんな型を生成しているか」を読めるようになるための良い練習です(実務ではimpl Iterator<Item = T> + 'aという書き方で具体型を隠すことも多いですが、まずは具体型を書き切れることが前提になります)。
💡 設計思想・なぜこう書くのか
3種類のIntoIteratorを型で使い分けるという設計は、Rustの「所有権の意図をシグネチャに刻む」という哲学の延長です。
for x in stack(Stack<T>を値渡し)と書いた瞬間、読み手は「このループの後、stackはもう使えない」とコードを読むだけでわかります。C++やJavaでは「このイテレーションはコレクションを破壊的に消費するのか、単に読むだけなのか」はドキュメントやメソッド名(drain()かどうか等)に頼るしかありませんが、Rustではfor x in stackとfor x in &stackの構文そのものが意味を持ちます- コンパイラは「
&mut stackで反復している最中に別の場所からstackを読み書きする」ことを借用チェッカーで機械的に禁止します。今日の3つの実装のうち②③はどちらも&'a Stack<T>/&'a mut Stack<T>という借用を受け取るため、ループが終わるまで元のstackへの他のアクセスがブロックされる、という保証が型システムレベルで得られます
&の有無)だけでコンパイラに伝えられるように設計する」という、Rust APIデザインの基本パターンです。標準ライブラリのほぼ全てのコレクション(Vec・HashMap・BTreeMap・HashSet…)がこの3点セットを踏襲しているのは、この設計哲学が一貫しているからです。🌐 他言語との比較
| 言語 | 「読むだけ」「書き換え」「消費」の区別 |
|---|---|
| Java | for (T x : collection)は常に読み取り専用のIterator<T>を返す。要素を書き換えるには明示的にインデックスアクセスするか、ListIterator.set()を使う必要があり、「消費するイテレーション」という概念自体が薄い(Iterator.remove()はあるが「コレクションを空にして所有権を返す」ものではない) |
| C++ | for (auto& x : container)の&の有無で参照/コピーは区別できるが、「コンテナを丸ごとムーブして消費する」ための専用構文はなく、std::move(container)を明示的に呼ぶ必要がある。またconst性(const auto&)と可変性(auto&)の区別はプログラマの自己申告であり、コンパイラが強制するわけではない |
| Go | スライスのfor rangeは常にコピー(値型なら値のコピー、参照型ならヘッダのコピー)で回るのが基本。ポインタを明示的に取らない限り要素の書き換えはできず、「消費するイテレーション」も言語機構としては存在しない |
| Rust | for x in v / for x in &v / for x in &mut vという構文上の違いが、それぞれ独立したIntoIterator実装として型システムに刻まれ、コンパイラが「消費後にvを使えないこと」まで検証する |
C++のfor (auto& x : v)とRustのfor x in &mut vは表面上似ていますが、Rustでは「その&mutの間、他の誰もvに触れない」ことをコンパイラが保証する点が本質的に異なります。C++にはこの保証はなく、実行時にイテレータ無効化などのバグとして現れます。
🏆 実務での使いどころ
- ドメインモデルのコレクション型(例:
OrderQueue、InventoryBatch)を自作するとき、Vecなどをラップしつつ、利用者にはfor order in &queue(閲覧)・for order in &mut queue(一括更新)・for order in queue(処理して破棄)という3通りの自然なAPIを提供できる - 設定ファイルをパースして得た
ConfigList<T>のようなラッパー型で、「読み取り専用の検証パス」と「値を消費して別の構造体へ変換するパス」を同じfor構文で書き分けられる - ゲームのインベントリシステムで「アイテム一覧を表示する(
&inventory)」「装備効果を一括適用する(&mut inventory)」「トレード相手に丸ごと譲渡する(inventoryを値で渡す)」という3つの操作を、呼び出し側のコードの見た目だけで区別できる
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
Iteratorさえ実装すればfor x in stack(値渡し)も自動的に使えると思い込む | IteratorとIntoIteratorの役割の違いを意識していない | forが直接呼ぶのはIntoIterator::into_iter()。Iteratorを実装しただけの型(Day 017〜023のCounterやIterMut等)をforに渡せているのは、標準ライブラリにimpl<I: Iterator> IntoIterator for I { type IntoIter = I; ... }という汎用実装があり、「イテレータ自身は自明にイテレータへ変換できる」ことになっているため |
&Stack<T>用のIntoIteratorを実装し忘れて、for x in &stackがコンパイルエラーになったときに原因がわからない | 「Stack<T>にIntoIteratorを実装したから、参照でも動くはず」と考えてしまう | Stack<T>と&Stack<T>と&mut Stack<T>はRustの型システム上は別々の型であり、それぞれに個別のimplが必要。標準コレクションで3つとも動くのは、標準ライブラリが3つとも実装しているからであって自動的に手に入るわけではない |
.rev()を使わずにself.items.into_iter()をそのまま返し、「下から上」の順で反復してしまう | 「スタックは要素を追加した順に並んでいる」というVecの内部表現をそのままイテレーション順だと思い込む | スタックの利用者が期待する順序(LIFO、=最後にpushしたものが先)と、内部のVecのメモリ上の並び順(先頭が最初のpush)は別物。データ構造の「利用者から見た意味」と「内部表現」を混同しないことが重要 |
for x in stackの後にもう一度stackを使おうとしてコンパイルエラーが出たとき、バグだと思って.clone()で無理やり回避しようとする | エラーメッセージ「value moved here」を見て「壊れた」と早合点する | これは意図した動作。「値渡しのforはコレクションを消費する」という設計そのものであり、その後stackを使いたいなら最初から&stackを使うべきだった、というのが正しい直し方(.clone()は要素のコピーコストが発生するため、本当に必要な場合以外は避ける) |
🚀 次のステップ
- 発展:
Stack<T>にdrain(&mut self) -> impl Iterator<Item = T> + '_を追加し、「Vec::drainのように、コレクション自体は空にするが所有権(変数束縛)は消費しない」第4のパターンを実装してみましょう。これは「値渡しで消費」でも「参照で読むだけ」でもない、実務でよく使われる中間パターンです - 次回予告: 自己評価で「完全理解」または「おおむね理解」が3回連続で記録された時点で、次のテーマスマートポインタ①(Box<T>)(ヒープ確保・再帰的データ構造)に進みます