Rustはシステムプログラミング言語として、メモリの安全性とパフォーマンスを高度に両立させています。
その中でも「配列(array)」は、固定長でスタックメモリ上に配置される非常に効率的なデータ構造です。
本記事では、Rustにおける配列の基本的な定義方法から、実務で重要となる高度なイテレート処理までを詳しく解説します。
静的な型システムを活かした安全なコードを書くために、配列の特性を深く理解することは極めて重要です。
Rustにおける配列(array)の基本概念
Rustの配列は、同じ型の要素をメモリ上に連続して配置するデータ構造です。
配列の最大の特徴は、コンパイル時にそのサイズが決定されており、実行中にサイズを変更できない点にあります。
この特性により、配列はヒープ領域ではなくスタック領域に直接割り当てられます。
スタック割り当ては非常に高速であり、パフォーマンスを重視する場面では配列の利用が推奨されます。
配列の型は [T; N] という形式で表現され、T は要素の型、N は非負の整数(サイズ)を指します。
配列の宣言と初期化
配列を宣言する際には、いくつかの方法が存在します。
最も一般的なのは、要素をカンマ区切りで列挙するリテラル表記です。
// 明示的な型指定を伴う配列の宣言
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// 型推論を利用した宣言
let implicit_numbers = [10, 20, 30];
また、すべての要素を同じ値で初期化する場合には、「[初期値; 個数]」という構文が便利です。
// すべての要素を0で初期化した長さ100の配列
let buffer = [0; 100];
この構文は、大きなバッファを確保する場合や、初期状態として特定のフラグを立てる際に多用されます。
配列の不変性と可変性
Rustの変数はデフォルトで不変(immutable)であるため、配列の要素を後から変更することはできません。
要素を書き換える必要がある場合は、宣言時に mut キーワードを付与します。
let mut mutable_array = [1, 2, 3];
// インデックスを指定して値を書き換える
mutable_array[0] = 10;
ただし、mut を付けた場合でも、配列のサイズそのものを変更することは不可能である点に注意してください。
サイズを動的に変更したい場合は、標準ライブラリの Vec<T> を検討する必要があります。
配列要素へのアクセスと安全性
配列内の要素にアクセスするには、インデックス記法 [] を使用します。
Rustではインデックスが 0 から始まります。
let fruits = ["apple", "banana", "cherry"];
let first = fruits[0];
ここで重要なのは、Rustが実行時に「境界チェック(Bounds Checking)」を行っている点です。
もし配列の範囲外(例えばサイズ3の配列に対してインデックス3以上)にアクセスしようとすると、プログラムは即座にパニック(実行時エラー)を引き起こします。
安全なアクセスを実現する get メソッド
パニックを回避し、より安全に要素を取得したい場合は、get メソッドを使用します。
get メソッドは、指定したインデックスが有効であれば Some(&T) を返し、無効であれば None を返します。
let data = [10, 20, 30];
match data.get(5) {
Some(value) => println!("値は {} です", value),
None => println!("インデックスが範囲外です"),
}
インデックスが範囲外です
このように Option 型を利用することで、予期せぬプログラムの停止を防ぐことができます。
実務においては、外部からの入力値に基づいて配列にアクセスする場合、この get メソッドの利用が強く推奨されます。
配列とスライスの関係
Rustの配列を扱う上で避けて通れないのが「スライス(slice)」という概念です。
スライスは、配列の全体または一部を参照するためのビュー(View)のような存在です。
型としては &[T] と表記され、サイズ情報を含んだポインタとして機能します。
配列のサイズは型の一部であるため、関数の引数に [i32; 5] を指定すると、サイズが5の配列しか受け取ることができません。
しかし、引数をスライス &[i32] にすることで、任意のサイズの配列を受け取ることが可能になります。
fn print_length(slice: &[i32]) {
println!("スライスの長さは {} です", slice.len());
}
let arr_small = [1, 2];
let arr_large = [1, 2, 3, 4, 5];
// 配列全体をスライスとして渡す
print_length(&arr_small);
print_length(&arr_large);
この柔軟性こそが、Rustで配列を実務的に扱うための鍵となります。
スライスは配列の一部だけを切り出すことも可能です。
let values = [0, 1, 2, 3, 4, 5];
let subset = &values[1..4]; // インデックス1から3までを参照
このようにスライスを活用することで、メモリのコピーを発生させずにデータの一部を効率的に操作できます。
実務で役立つ配列のイテレート手法
配列内の要素を一つずつ処理する際、Rustでは多様なイテレート(反復)手法が用意されています。
単なる for ループから、関数型プログラミングのようなメソッドチェーンまで、用途に応じて使い分けることが求められます。
基本的な for ループ
最も直感的なのは for ループを使用した反復処理です。
let items = [10, 20, 30];
for item in items {
println!("要素: {}", item);
}
Rust 1.53以降、配列そのものを for ループに渡すと、IntoIteratorが実装されているため、要素の所有権を移動(move)させながら処理できます。
所有権を移動させたくない(参照だけしたい)場合は、&items と記述するか、iter() メソッドを使用します。
iter(), iter_mut(), into_iter() の使い分け
配列のイテレータを生成するメソッドは主に3種類あります。
| メソッド | 説明 | 取得される型 |
|---|---|---|
iter() | 不変の参照を生成する | &T |
iter_mut() | 可変の参照を生成する | &mut T |
into_iter() | 所有権を持つ値を生成する(または参照) | T (または状況による) |
可変の参照を必要とするケースでは、iter_mut() が不可欠です。
let mut scores = [10, 20, 30];
for score in scores.iter_mut() {
*score += 5; // デリファレンスして値を更新
}
イテレータアダプタの活用
Rustのイテレータは非常に強力で、多くの便利なメソッド(アダプタ)が備わっています。
map や filter、fold を組み合わせることで、複雑な集計処理を簡潔に記述できます。
let numbers = [1, 2, 3, 4, 5];
// 偶数だけを抽出し、それらを2倍にした合計を求める
let result: i32 = numbers.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.sum();
println!("結果: {}", result);
結果: 12
このようなメソッドチェーンは、「ゼロコスト抽象化」によって最適化されるため、手動で書いたループと同等のパフォーマンスを発揮します。
高度な配列操作:ウィンドウとチャンク
実務のアルゴリズム実装において、配列の要素を「一定の幅」でスライドさせたり、分割して処理したりしたい場面があります。
スライスが提供する windows や chunks メソッドを使用すると、これらを簡単に実現できます。
windows メソッド
windows(n) は、サイズ n の重なり合う連続した部分スライスを返します。
let data = [1, 2, 3, 4];
for win in data.windows(2) {
println!("{:?}", win);
}
[1, 2]
[2, 3]
[3, 4]
これは、時系列データの差分計算や、移動平均の算出などに非常に有用です。
chunks メソッド
対照的に chunks(n) は、重なり合わない固定サイズのグループに分割します。
let data = [1, 2, 3, 4, 5];
for chunk in data.chunks(2) {
println!("{:?}", chunk);
}
[1, 2]
[3, 4]
[5]
ネットワーク通信のパケット分割や、バッチ処理の実装において頻繁に利用されます。
配列を使用する際の注意点と制限
Rustの配列は強力ですが、その性質ゆえの制約も存在します。
まず、配列の要素数はコンパイル時の定数である必要があるという点です。
変数の値をサイズに指定することはできず、必ず const またはリテラルでなければなりません。
const SIZE: usize = 10;
let my_arr: [i32; SIZE] = [0; SIZE]; // これはOK
次に、大きなサイズの配列をスタックに確保しようとすると、スタックオーバーフローを引き起こすリスクがあります。
スタックメモリのサイズはOSや環境によって制限されているため、数メガバイト単位のデータを扱う場合は Box<[T; N]> や Vec<T> を使用してヒープ領域を確保するのが定石です。
また、要素が Copy トレイトを実装していない場合、一括初期化 [value; N] が制限されることがあります。
その場合は、std::array::from_fn などの新しいAPIを活用して動的に初期化を行う手法が有効です。
まとめ
Rustの配列は、固定長で型安全、そして極めて高いパフォーマンスを持つ基本的なデータ構造です。
スタックメモリへの直接配置という特性を活かし、メモリ効率の最適化を図る際に大きな威力を発揮します。
一方で、その厳格なサイズ制限を補完するために、スライス(&[T])との相互変換を使いこなすことが実務上の必須スキルとなります。
さらに、iter() メソッドをはじめとする強力なイテレータ機能を活用することで、安全かつ宣言的にデータを操作できるようになります。
今回解説した基本的な宣言から、windows や chunks といった高度なテクニックまでを組み合わせ、より堅牢なRustアプリケーションの構築に役立ててください。
配列の仕組みを正しく理解することは、Rustのメモリモデルを深く知るための第一歩となるでしょう。
