Rustの配列型(Array)を完全攻略!固定長データ構造の特徴とベクタ・スライスとの違い
生徒
「Rustで複数のデータをまとめたい時、まずは何を使えばいいんでしょうか?配列という言葉はよく聞きますが……」
先生
「Rustにはいくつかのコレクションがありますが、基本となるのは配列(Array)です。ただし、Rustの配列は他の言語と違って『サイズが固定』という強い特徴があるんですよ。」
生徒
「サイズが固定だと、後から要素を増やしたりはできないってことですか?」
先生
「その通りです。その代わり、メモリ効率が非常に良く、スタック領域に確保されるため高速に動作します。まずは配列の宣言方法から詳しく解説していきますね。」
1. Rustの配列型とは何か
Rustにおける配列(Array)とは、同じデータ型の値を複数、一列に並べて管理するためのデータ構造です。最大の特徴は、コンパイル時にその要素数(長さ)が決まっており、実行中にサイズを変更することができない「固定長」である点にあります。例えば、5つの整数を格納する配列を作成した場合、その配列はずっと5つの要素を持ち続け、6つ目を追加したり3つに減らしたりすることはできません。
この性質は一見不便に感じるかもしれませんが、コンピュータのメモリ管理の視点からは非常に優秀です。配列はメモリ上の「スタック」と呼ばれる領域に連続して配置されるため、データの読み書きが極めて高速に行われます。また、Rustのコンパイラは配列のサイズを正確に把握しているため、範囲外アクセスなどのエラーを事前に検出しやすく、プログラムの安全性を高める役割も果たしています。用途としては、月の日数や曜日のリストなど、あらかじめ個数が決まっているデータを扱うのに最適です。
2. 配列の基本的な宣言と初期化の方法
Rustで配列を宣言する際には、格納する値の型と要素数を指定します。記述方法は [型; 要素数] という独特な構文を使用します。例えば、i32型の整数を3個持つ配列であれば [i32; 3] と定義します。この型指定は、要素数が一つでも異なれば別の型として扱われるため、非常に厳格な型システムの一部となっています。
配列の初期化には、直接値を並べる方法と、同じ値で埋める方法の二種類があります。特に後者の「一括初期化」は、大きなサイズの配列をゼロクリアしたい場合などに非常に便利です。具体的な書き方を以下のコードで確認してみましょう。
fn main() {
// 方法1: 具体的な値を並べて初期化
let numbers: [i32; 3] = [10, 20, 30];
// 方法2: 同じ値で全要素を埋める(初期値 0 を 5個)
let buffer = [0; 5];
println!("一つ目の要素: {}", numbers[0]);
println!("バッファの全内容: {:?}", buffer);
}
3. インデックスによる要素へのアクセスと安全性
配列内の特定の要素を取り出すには、インデックス(添字)を使用します。Rustのインデックスは「0」から始まります。つまり、3つの要素がある配列の場合、アクセスできるのは0番、1番、2番となります。ここで重要なのが、Rustの安全性へのこだわりです。もし存在しないインデックス(例えば3番など)にアクセスしようとすると、プログラムはどうなるでしょうか。
多くの言語では不正なメモリを参照して深刻なバグになりますが、Rustでは実行時に「境界チェック」が行われます。範囲外へのアクセスを検知すると、プログラムは安全に「パニック(強制終了)」します。これにより、メモリを破壊してデータが漏洩したり、システムが不安定になったりすることを未然に防いでいます。開発者は常に正しい範囲内でアクセスするようにコードを書く習慣が身につきます。
fn main() {
let days = ["月", "火", "水", "木", "金", "土", "日"];
// 3番目(木曜日)を表示
let target_index = 3;
println!("指定した曜日は: {}", days[target_index]);
// もし days[10] のようにアクセスすると実行時にエラー(panic)になります
}
4. 配列をループ処理で効率的に扱う
配列の全要素に対して何らかの処理を行いたい場合、ループ(繰り返し処理)を使用するのが一般的です。Rustでは for 文を使って非常に簡潔に書くことができます。配列そのものをループに回すこともできますが、一般的には要素への「参照」を取得して処理することが推奨されます。
配列の各要素を順に処理する際、インデックスを自分で管理する必要がないため、書き間違いによるバグを減らすことができます。また、Rustのイテレータという仕組みを組み合わせることで、データの集計や加工もスムーズに行えます。基本的なループの書き方を見てみましょう。
fn main() {
let scores = [85, 92, 78, 64, 100];
let mut sum = 0;
// 配列の要素を一つずつ取り出して合計を計算
for score in scores.iter() {
sum += score;
}
let average = sum as f64 / scores.len() as f64;
println!("合計点: {}, 平均点: {}", sum, average);
}
5. スタック領域への配置とパフォーマンス
コンピュータのメモリには、大きく分けて「スタック(Stack)」と「ヒープ(Heap)」があります。Rustの配列は原則としてスタック領域に割り当てられます。スタックはLIFO(後入れ先出し)形式で管理される非常に高速なメモリ領域です。データのサイズがコンパイル時に決まっているため、OSは実行時にメモリを探す手間を省き、即座に領域を確保できます。
この「スタック配置」こそが配列の最大のメリットです。大量の小さなデータを頻繁に読み書きする場合、ヒープ領域を使うベクタ(Vector)よりも配列の方がパフォーマンス面で有利になることがあります。ただし、スタックは容量に限りがあるため、数ギガバイトに及ぶような超巨大な配列を作ると「スタックオーバーフロー」を引き起こす可能性がある点は注意が必要です。適切なサイズを見極めることが重要です。
6. ベクタ(Vector)との決定的な違い
初心者の方が最も迷うのが「配列とベクタの使い分け」です。ベクタ(Vec<T>)は、配列とは異なり「可変長」のデータ構造です。実行中に要素を push して増やしたり、削除したりできます。ベクタのデータはヒープ領域に保存されるため、サイズを柔軟に変更できるのです。
まとめると、**「要素数が絶対に変わらないなら配列」**、**「ユーザーの入力などで要素数が変わるならベクタ」**という基準で選ぶのが正解です。現代のソフトウェア開発ではデータの個数が動的に決まることが多いため、実際にはベクタの方が利用頻度は高いですが、基盤となる配列の知識はRustを使いこなす上で欠かせません。以下の表で違いを確認しましょう。
| 特徴 | 配列(Array) | ベクタ(Vector) |
|---|---|---|
| サイズ | 固定(コンパイル時決定) | 可変(実行時変更可能) |
| メモリ領域 | スタック | ヒープ |
| 宣言例 | [1, 2, 3] |
vec![1, 2, 3] |
7. スライスとの関係性を理解する
配列を語る上で避けて通れないのが「スライス(Slice)」です。スライスは配列やベクタの「一部分」を指し示す参照のようなものです。型は &[T] と書きます。スライス自体はデータを持っていないので、配列の特定の範囲だけを関数に渡して処理したい、といった場面で使われます。
例えば、10個の要素がある配列の「真ん中の3個だけ」を別の関数で見せたい時、配列全体をコピーするのは非効率です。スライスを使えば、メモリ上の位置と長さの情報だけを渡すことができ、非常に軽量で安全なデータ共有が可能になります。配列はスライスへ簡単に型変換(強制)されるため、関数の引数には配列そのものではなくスライス型を指定するのがRustのベストプラクティスです。
// 配列の一部(スライス)を受け取って表示する関数
fn show_part(data: &[i32]) {
println!("スライスの内容は: {:?}", data);
}
fn main() {
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// インデックス1から4の手前まで(1, 2, 3)を切り出す
let sliced_data = &arr[1..4];
show_part(sliced_data);
}
8. 配列の多次元的な利用方法
配列は一次元だけでなく、多次元、つまり「配列の配列」として定義することも可能です。これはゲームのマップデータや数学の行列計算など、格子状のデータを扱う際に非常に役立ちます。二次元配列の場合、型は [[型; 列数]; 行数] のようになります。
アクセスする際は matrix[row][col] のように、二つのインデックスを指定します。多次元配列も一次元と同様に、すべての要素数が固定されている必要があります。複雑に見えますが、メモリ上では一直線に並んでおり、Rustが計算によって目的の場所に案内してくれます。初心者の方は、まずは二次元配列から触れてみて、データの広がりを実感してみると良いでしょう。
9. 配列を引数として関数に渡す際の注意点
配列を関数に渡す際には、少し注意が必要です。Rustでは配列をそのまま引数に渡すと、値の「コピー」が発生します(要素が少数の場合)。しかし、前述の通り配列の型には要素数が含まれるため、fn func(a: [i32; 3]) という関数に [i32; 5] の配列を渡すことはできません。
この制限を回避するために、実務ではほとんどの場合、配列全体を「スライス参照」として渡します。fn func(a: &[i32]) と定義すれば、どんな長さの配列でも(そしてベクタでも!)受け取れるようになります。この柔軟性はRustの非常に強力な点であり、コードの再利用性を高める鍵となります。初心者のうちは「引数には & をつける」と覚えておくだけでも、多くのコンパイルエラーを回避できるはずです。
10. 配列の初期化における制約と回避策
Rustの配列には「すべての要素が同じ型でなければならない」というルールのほかに、「未初期化の状態を許さない」という厳しいルールがあります。これはメモリ安全性を保つための鉄則です。しかし、ときには「最初は空の状態にして、後から計算して埋めたい」という場面もあります。その場合、配列のサイズが大きすぎると [0; 1000] のようにデフォルト値で埋めるのにもコストがかかります。
高度な手法として MaybeUninit という仕組みもありますが、初心者の方はまず、初期化が難しいと感じたら「ベクタ(Vector)」の使用を検討してください。ベクタなら空の状態から始めて、必要な分だけ push していくことができます。Rustを学ぶ上で、一つの型に固執せず、用途に合わせて最適なデータ構造を選べるようになることが、脱初心者への近道と言えるでしょう。配列の固定長という個性を理解し、その高速性と安全性を最大限に活かしたプログラミングを楽しんでください。
Rustの配列は、非常にシンプルながらも奥が深い仕組みです。固定長であることの不便さを、安全性とパフォーマンスが補っています。この記事を通じて、配列の宣言から活用方法、そしてベクタやスライスとの違いをイメージできるようになったなら幸いです。実際に手を動かして、コードをコンパイルしてみることが一番の学習になります。ぜひ、自分でも様々なサイズの配列を作って遊んでみてください!