Rustにおいて、データの集合を効率的に処理するために欠かせないのがイテレータの存在です。
しかし、ソースコードを書いている中で、iter() を使うべきか into_iter() を使うべきか迷う場面は少なくありません。
これら2つのメソッドの最大の違いは、Rustの根幹をなす概念である「所有権」と「借用」の扱いにあります。
本記事では、Rustにおけるイテレータの仕組みを整理し、iter() と into_iter() の違いを明確に解説します。
さらに、具体的なコード例や使い分けのポイントを紹介することで、実務でのコーディングに役立つ知識を提供します。
Rustにおけるイテレータの基本概念
イテレータとは、一連の要素に対して順番にアクセスするための仕組みを提供するトレイトです。
Rustの標準ライブラリでは、Iterator トレイトを実装することで、様々なデータ構造に対して共通の操作を行うことができます。
イテレータの役割と重要性
イテレータを使用する主な利点は、ループ処理を抽象化し、コードの可読性と安全性を高められる点にあります。
Rustのイテレータは「ゼロコスト抽象化」の一つであり、手動でインデックスを操作するループと比較してもパフォーマンスが劣ることはありません。
むしろ、境界チェックを最適化できるため、より効率的な実行が期待できる場合も多いです。
IntoIteratorトレイトとの関係
iter() や into_iter() を理解する上で避けて通れないのが IntoIterator トレイトです。
このトレイトは、ある型をイテレータに変換する機能を提供しており、Rustの for ループの背後で暗黙的に利用されています。
for ループにコレクションを渡すと、コンパイラは自動的に into_iter() メソッドを呼び出します。
iter() と into_iter() の決定的な違い
ここからは、本題である iter() と into_iter() の違いを深掘りしていきましょう。
結論から述べると、両者の違いは「イテレータが要素をどのように保持するか」という点に集約されます。
iter() の特徴:不変の参照による借用
iter() メソッドは、コレクションの各要素に対する不変の参照を生成します。
つまり、元のコレクションの所有権は移動せず、データ自体もコピーされません。
そのため、iter() を使用した後でも、元のコレクションを再び利用することが可能です。
この挙動は、データを読み取るだけで変更する必要がない場合に最適です。
into_iter() の特徴:所有権の移動と消費
一方で into_iter() は、コレクションそのものを消費し、各要素の所有権を移動させます。
メソッドを実行すると元のコレクションは利用不可となり、イテレータが値を「所有」する形になります。
値を別の型に変換したり、関数の引数として所有権ごと渡したりする場合には、この into_iter() が必要になります。
「into」という名前が示す通り、元のデータをイテレータへと変化させるイメージを持つと理解しやすいでしょう。
iter_mut() との比較
これら2つに加え、頻繁に使用されるのが iter_mut() です。
iter_mut() は、各要素に対する可変の参照を生成します。
元のコレクションの所有権は維持しつつ、ループ内で要素の値を直接書き換えたい場合に使用します。
メモリモデルから見た挙動の整理
それぞれのメソッドがどのような型を返すのかを、表で整理して確認してみましょう。
| メソッド | 返り値の要素型 | 元のコレクションの状態 | 主な用途 |
|---|---|---|---|
iter() | &T (不変参照) | 利用可能 | データの読み取り、集計 |
iter_mut() | &mut T (可変参照) | 利用可能 | データのインプレース更新 |
into_iter() | T (所有権の移動) | 利用不可 (消費される) | データの変換、フィルタリング後の再構築 |
このように、所有権のセマンティクスによって明確に役割が分かれていることがわかります。
実践的なコード例による動作検証
具体的なプログラムを用いて、挙動の違いを実際に確認してみましょう。
iter() を使用した借用の例
まずは、iter() を使用してベクタ内の要素を読み取る例です。
fn main() {
let v = vec![1, 2, 3];
// iter() は要素への参照 (&i32) を返す
for val in v.iter() {
println!("値: {}", val);
}
// 所有権が移動していないため、v を再度使用できる
println!("ベクタの長さ: {}", v.len());
}
値: 1
値: 2
値: 3
ベクタの長さ: 3
この例では、v.iter() を呼び出した後も v.len() を呼び出すことができています。
これは iter() が &v を借用しているだけであり、所有権を奪っていないことを示しています。
into_iter() を使用した所有権移動の例
次に、into_iter() を使用した場合の挙動を見てみましょう。
fn main() {
let v = vec![1, 2, 3];
// into_iter() は要素そのもの (i32) を返す
for val in v.into_iter() {
println!("値: {}", val);
// ここで val の所有権を持っている
}
// 以下の行はコンパイルエラーになる
// println!("ベクタの長さ: {}", v.len());
}
このコードをコンパイルしようとすると、Rustコンパイラは「borrow of moved value」というエラーを出力します。
into_iter() によって v 自体が消費され、ループ終了後には v は有効な変数ではなくなっているためです。
暗黙的な into_iter() の呼び出し
Rustの for ループでは、対象が IntoIterator を実装していれば直接渡すことが可能です。
fn main() {
let v = vec![1, 2, 3];
// v を直接渡すと into_iter() が自動的に呼ばれる
for val in v {
println!("{}", val);
}
}
注意点として、コレクションそのものを for ループに渡すと所有権が移動してしまいます。
もし所有権を移動させたくない場合は、for val in &v と記述します。
これにより、&Vec<T> 型に対して into_iter() が呼ばれ、結果として iter() と同じく参照を返すようになります。
どちらを使うべきか?判断基準のポイント
日常的な開発において、どちらを選択すべきか迷った際のガイドラインを提示します。
データの再利用が必要な場合
ループ処理の後にも同じコレクションを使い続けたい場合は、迷わず iter() を使用してください。
例えば、ログの出力や数値の合計計算などは、データの参照だけで事足ります。
iter() を使うことで、不要なメモリ確保や所有権の複雑さを回避できます。
パフォーマンスとメモリ効率を優先する場合
巨大な構造体を含むベクタなどを処理し、その後に元のベクタが不要な場合は into_iter() が有利です。
into_iter() は要素をコピーせず、ポインタの移動だけで済むためです。
また、map などのアダプタを組み合わせて新しいコレクションを作る場合も、所有権を移動させた方が効率的です。
複雑なライフサイクルの回避
参照 (&T) を扱う場合、Rustの借用チェッカーによってライフサイクルの管理が厳しく求められることがあります。
特に関数からイテレータを返すようなケースでは、参照よりも所有権を持った値 (T) を返す into_iter() の方が設計が容易になることが多いです。
よくある間違いとトラブルシューティング
イテレータを扱う際によく遭遇する問題について触れておきます。
クロージャ内での所有権
iter().map(|x| ...) のようにメソッドチェーンを使う際、クロージャが周囲の変数をキャプチャすることがあります。
このとき、iter() は参照を渡すため、クロージャ内で所有権が必要な操作 (例えばStringの結合など) を行うには、明示的な clone() や to_owned() が必要になる場合があります。
もし元のデータが不要であれば、最初から into_iter() を使うことで、余計なクローンを減らすことができます。
参照の参照問題
&Vec<T> に対して iter() を呼ぶと、二重の参照が発生しているように見えることがありますが、Rustのデリファレンス型強制によって通常は適切に扱われます。
しかし、複雑なジェネリクスを扱う際には、自分が今「値」を扱っているのか「参照」を扱っているのかを常に意識することが重要です。
まとめ
Rustにおける iter() と into_iter() の違いは、プログラムの安全性と効率性に直結する重要な要素です。
iter() は「データを一時的に借りる」ためのメソッドであり、元のコレクションを破壊しません。
一方、into_iter() は「データを使い切る」ためのメソッドであり、所有権を移動させてコレクションを消費します。
これらの違いを正しく理解し、借用が必要な場面と所有権が必要な場面を切り分けることが、高品質なRustコードを書くための第一歩です。
まずは iter() から使い始め、コンパイルエラーが出た際やパフォーマンスを最適化したいタイミングで into_iter() を検討すると良いでしょう。
Rustの厳格な所有権システムは最初は難しく感じられますが、使いこなせるようになればメモリ安全なプログラムを自信を持って構築できるようになります。
