Rustのfor構文とイテレータを徹底解説!繰り返し処理とメモリ安全なループの仕組み
生徒
「Rustで配列の中身を順番に表示させたいんですが、forループの使い方が他の言語と少し違う気がします。イテレータって何ですか?」
先生
「鋭いですね。Rustのfor構文は、実はイテレータ(Iterator)という仕組みと深く結びついています。単に繰り返すだけでなく、メモリの安全性を守りながら効率よくデータを処理するための工夫が詰まっているんですよ。」
生徒
「メモリの安全性も関係しているんですね。具体的にどう書けば安全にループを回せるんでしょうか?」
先生
「Rustではインデックスによるアクセスよりも、イテレータを使う方が推奨されます。範囲指定やコレクションの反復処理など、基本的なパターンから応用まで順番に解説していきますね!」
1. Rustにおける制御構文の基本とループの重要性
プログラミングにおいて、特定の処理を何度も繰り返す「ループ処理」は欠かせない要素です。Rustには、条件が満たされるまで繰り返すwhile、無限ループを作るloop、そして最も多用されるforの三種類の制御構文が存在します。特にfor構文は、配列やベクトルといったデータの集まり(コレクション)を安全に走査するために設計されています。
他の言語、例えばC言語などでは「変数 i を 0 から 10 まで増やしていく」という書き方が一般的ですが、Rustではこの「数値の管理」を手動で行うことを極力避けます。なぜなら、手動で添え字(インデックス)を管理すると、範囲外アクセスによるクラッシュやセキュリティホールの原因になるからです。Rustのfor構文は、次に説明する「イテレータ」という仕組みを内部で利用することで、これらのリスクをコンパイルレベルで排除しています。
2. イテレータの概念とRustが安全な理由
イテレータ(Iterator)とは、一言で言えば「集合の中にある要素を順番に取り出す仕組み」のことです。Rustの標準ライブラリにはIteratorトレイトが用意されており、これに基づいたオブジェクトは「次の要素をください」という要求に応えることができます。
Rustのforループが安全だと言われる最大の理由は、イテレータが「境界チェック」の負担を減らしてくれる点にあります。配列のサイズをプログラマが意識しなくても、イテレータが自動的に最後の要素までを把握し、終わればループを終了させてくれます。これにより、メモリの安全性(メモリセーフ)が保たれ、バグの混入を防ぐことができるのです。また、イテレータは「遅延評価」という特性を持っており、必要になるまで計算を行わないため、パフォーマンス面でも非常に優れています。
3. 範囲指定を使った基本的なforループの使い方
まずは、指定した回数だけ処理を繰り返す最もシンプルな方法を見てみましょう。Rustではstart..endという範囲指定(Range)記法を使います。
fn main() {
// 1から4まで(5は含まない)繰り返す
for number in 1..5 {
println!("カウントアップ: {}", number);
}
println!("---");
// 1から5まで(5を含む)繰り返す
for number in 1..=5 {
println!("5まで数えるよ: {}", number);
}
}
カウントアップ: 1
カウントアップ: 2
カウントアップ: 3
カウントアップ: 4
---
5まで数えるよ: 1
5まで数えるよ: 2
5まで数えるよ: 3
5まで数えるよ: 4
5まで数えるよ: 5
上記のコードでは、1..5という記法が自動的にイテレータとして振る舞います。末尾の数値を含めたい場合は、..=という記号を使います。非常に直感的で、数値のインクリメント(加算)を書き忘れて無限ループに陥る心配もありません。初心者が最初に覚えるべき、基本のループパターンです。
4. 配列やベクタとイテレータの連携
次に、実際の開発でよく使われる「データのリスト」を処理する方法を学びましょう。Rustの配列やVec(可変長配列)から要素を取り出す際は、直接forに渡すことができます。しかし、ここで重要になるのが「所有権」の概念です。通常、コレクションをそのままforに入れるとそのデータの所有権が移動してしまいますが、多くの場合は参照(借用)を使って読み取り専用で処理します。
fn main() {
let fruits = vec!["リンゴ", "バナナ", "オレンジ"];
// iter() メソッドを使って要素への参照を取得する
for fruit in fruits.iter() {
println!("大好きな果物: {}", fruit);
}
// 元のリストはまだ使える(所有権が移動していないため)
println!("果物の数は全部で{}個です。", fruits.len());
}
大好きな果物: リンゴ
大好きな果物: バナナ
大好きな果物: オレンジ
果物の数は全部で3個です。
iter()メソッドを使うことで、リストの各要素に対する「読み取り権限」だけを借りてループを回せます。もし、ループ内で要素の中身を書き換えたい場合は、後述するiter_mut()を使用します。このように、Rustではループの回し方ひとつとっても、そのデータが「読み取り専用なのか」「書き込み可能なのか」「使い捨てなのか」を明確に区別します。
5. 要素を書き換えるための可変イテレータ
ループの中で配列の内容を変更したい場面もあります。その場合は、&mut(可変参照)を生成するiter_mut()を使用します。これにより、メモリの安全性を保ったまま、元のデータを直接操作することが可能です。
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
// 各要素を2倍にする
for num in numbers.iter_mut() {
*num *= 2; // デリファレンス(*)して値を書き換える
}
println!("2倍になった配列: {:?}", numbers);
}
2倍になった配列: [2, 4, 6, 8, 10]
この例では、numは要素そのものではなく、要素への「変更可能な参照」です。そのため、*numと書いて元の場所にアクセスし、値を更新しています。Rustの厳格な借用規則により、このループが動いている間は他の場所からnumbersを書き換えることができないため、並行処理などでも安全性が担保されます。データ競合を未然に防ぐこの仕組みこそが、Rustの真骨頂です。
6. ステップ実行と逆順での繰り返し処理
時には、一つ飛ばしで処理したり、後ろから順番に処理したいこともあるでしょう。Rustのイテレータには、こうした要望に応えるための便利なメソッドが多数用意されています。代表的なものがstep_byとrevです。
fn main() {
println!("逆順でカウントダウン:");
// rev() で範囲を反転させる
for i in (1..=3).rev() {
println!("{}!", i);
}
println!("2つずつ飛ばして表示:");
// step_by(2) で2ステップずつ進む
for i in (0..10).step_by(2) {
println!("偶数: {}", i);
}
}
逆順でカウントダウン:
3!
2!
1!
2つずつ飛ばして表示:
偶数: 0
偶数: 2
偶数: 4
偶数: 6
偶数: 8
これらは「アダプタ」と呼ばれ、元のイテレータを加工して新しい動作を定義します。複雑な条件をループの内部(if文など)で書くのではなく、イテレータの段階で定義することで、コードの見通しが非常に良くなります。これも可読性と保守性を高めるためのRustらしい書き方と言えます。
7. enumerateを使ったインデックス付きのループ
「今何回目のループか」を知るために、昔ながらのカウンタ変数を用意する必要はありません。enumerate()メソッドを使えば、要素と一緒に現在のインデックス(0から始まる番号)をペアで受け取ることができます。これは、リストの内容を表示する際に番号を振りたいときなどに非常に便利です。
fn main() {
let languages = vec!["Rust", "Python", "Go", "TypeScript"];
// (インデックス, 要素) のタプルで受け取る
for (i, lang) in languages.iter().enumerate() {
println!("第{}位: {}言語", i + 1, lang);
}
}
第1位: Rust言語
第2位: Python言語
第3位: Go言語
第4位: TypeScript言語
タプル展開という機能を使って(i, lang)と書くことで、番号と値をスマートに分離して受け取っています。手動でi += 1と書く必要がないため、ミスの入り込む余地がありません。Rustにおける効率的で安全なコーディングスタイルの好例です。
8. mapやfilterによる関数型プログラミング的な処理
Rustのイテレータは、forループで使うだけでなく、メソッドを繋げていくことで複雑なデータ変換を行うことも得意です。例えば、リストの中から特定の条件に合うものだけを抽出し(filter)、それらを加工(map)して新しいリストを作る、といった処理を流れるように記述できます。
これは「関数型プログラミング」のスタイルですが、Rustではこれがゼロコスト抽象化、つまり手動でループを書くのと同等の速度で動作するように最適化されています。初心者のうちは少し難しく感じるかもしれませんが、for構文とイテレータの関係に慣れてくると、このメソッドチェーンの便利さが病みつきになるはずです。データの変換処理において、一時的な変数を減らすことができるため、副作用の少ない綺麗なコードになります。
9. 初心者が陥りやすいイテレータの注意点
Rustの学習を始めたばかりの人が最初につまずくのは、やはり「所有権」に関連するエラーです。例えば、into_iter()というメソッドを使うと、コレクションそのものを消費してしまい、ループの後にその変数を使うことができなくなります。これは「値を移動(ムーブ)」させるためです。
一方で、今回メインで紹介したiter()や&arrという書き方は「借用」であり、所有権を維持したまま中身を覗き見ることができます。エラーメッセージで「value borrowed here after move(移動後に借用されています)」といった内容が出たときは、自分がイテレータをどう生成したかを確認してみてください。適切なメソッドを選ぶことが、Rustを乗りこなす第一歩です。