Rustの配列・ベクタ・スライスを徹底解説!違いと使い分けを完全マスター
生徒
「Rustで複数のデータをまとめて扱いたいとき、配列とかベクタとか色々あってどれを使えばいいか迷っちゃいます。何が違うんですか?」
先生
「それはRust学習者が最初に通る道ですね。Rustには主に配列(Array)、ベクタ(Vector)、そしてスライス(Slice)の3種類があります。これらはメモリの管理方法やデータの柔軟性が全く違うんですよ。」
生徒
「メモリの管理方法ですか……難しそうですね。初心者でも使い分けられるようになりますか?」
先生
「大丈夫です!『サイズが変わるかどうか』や『データの持ち主が誰か』というポイントさえ押さえれば、自然と使い分けられるようになります。それぞれの特徴を詳しく見ていきましょう!」
1. Rustの配列は固定長でスタック領域に保存される
Rustにおける配列(Array)は、同じ型の要素を複数並べたデータ構造です。最大の特徴は、**「一度決めたサイズを後から変更できない」**という点にあります。例えば、5つの要素を持つ配列を作ったら、後から6つに増やすことはできません。これは、配列のサイズがコンパイル時に確定しており、メモリ上のスタック領域という高速にアクセスできる場所に直接配置されるためです。
配列は、型定義において [型; 要素数] という形式で記述されます。要素数が型の一部として扱われるため、[i32; 3] と [i32; 4] は全く別の型として認識されます。この厳格さが、プログラムの安全性を高める要因となっています。週末の曜日リストや、数学的な行列など、データの個数が絶対に変わらない場合に適しています。
fn main() {
// 固定長の配列を定義。型は [i32; 5] になる
let numbers: [i32; 5] = [10, 20, 30, 40, 50];
// インデックスでアクセス(0から始まる)
println!("最初の要素: {}", numbers[0]);
println!("配列の長さ: {}", numbers.len());
// 初期値を指定して全要素を埋める方法
let all_zeros = [0; 100]; // 0が100個並んだ配列
println!("100番目の要素: {}", all_zeros[99]);
}
2. 動的なデータ操作に欠かせないベクタ
次に紹介するのが、実務で最も頻繁に使用されるベクタ(Vector)です。ベクタは配列とは対照的に、**「要素を後から自由に追加・削除できる」**という柔軟性を持っています。標準ライブラリの Vec<T> 型として提供されており、データはヒープ領域という自由度の高いメモリ領域に保存されます。
ベクタを使用する際は、要素の追加(push)や削除(pop)が可能です。ユーザーからの入力データを受け取ったり、データベースから取得したレコードを格納したりする場合など、実行してみるまでデータの個数が分からないシーンで大活躍します。Rustではマクロ vec! を使うことで、配列のように簡単に初期化することができます。
fn main() {
// 空のベクタを作成(型推論により Vec<i32> になる)
let mut fruits = vec!["りんご", "バナナ"];
// 要素を追加
fruits.push("オレンジ");
fruits.push("メロン");
println!("果物リスト: {:?}", fruits);
// 要素を削除(最後の一つを取り出す)
let last = fruits.pop();
println!("取り出した果物: {:?}", last);
println!("現在のリスト: {:?}", fruits);
}
3. データの窓口としてのスライスの役割
スライス(Slice)は、少し特殊な存在です。スライス自体がデータを持っているわけではなく、配列やベクタなどの**「連続したメモリ領域の一部を指し示す参照」**です。そのため、スライスは所有権を持ちません。型としては &[T] と表現され、ポインタ(データの場所)と長さの情報だけを保持しています。
なぜスライスが必要なのでしょうか?それは、関数にデータを渡す際に、相手が「配列」なのか「ベクタ」なのかを気にせずに済むようにするためです。スライスを引数に取る関数を作れば、どちらのデータ構造からも必要な部分だけを切り出して安全に渡すことができます。これを「参照」として扱うことで、巨大なデータをコピーすることなく効率的に処理できるのが大きなメリットです。
fn main() {
let numbers = [1, 2, 3, 4, 5];
// 配列の2番目から4番目までを参照するスライスを作成
// インデックス 1番目から 4番目の手前まで(1, 2, 3)
let slice: &[i32] = &numbers[1..4];
println!("スライスの内容: {:?}", slice);
println!("スライスの長さ: {}", slice.len());
}
4. 配列とベクタとスライスの決定的な違い
初心者の方が整理しやすいように、これら3つの違いを「メモリ」「サイズ」「所有権」の観点から比較してみましょう。まず、**配列**はスタックに置かれ、サイズは固定です。一度決めたら変更できず、そのスコープが終われば自動的に破棄されます。非常に高速ですが、柔軟性には欠けます。
一方、**ベクタ**はヒープに置かれ、サイズは動的です。必要に応じてメモリを再確保して拡大します。所有権を持っているため、ベクタ変数がスコープを抜けると、ヒープ上のデータもすべて解放されます。最後に**スライス**ですが、これはスタック上に「どこからどこまでを見るか」という情報だけを置きます。元のデータが配列であってもベクタであっても、その一部分を「借りる」役割を果たします。
| 特徴 | 配列 (Array) | ベクタ (Vector) | スライス (Slice) |
|---|---|---|---|
| 型表記 | [T; N] |
Vec<T> |
&[T] |
| サイズ | コンパイル時に固定 | 実行時に変更可能 | 実行時に決まる(参照) |
| 保存場所 | スタック | ヒープ | スタック(参照情報のみ) |
| 所有権 | あり | あり | なし(借用) |
5. 実践的な使い分けの判断基準
では、日常のコーディングでどのように使い分ければよいのでしょうか。基本的なルールは**「まずはベクタを使う」**ことです。ほとんどのアプリケーションではデータの数は変動するため、Vec<T> が最も汎用的で安全です。設定値や固定の変換テーブルなど、プログラムの実行中に絶対に要素数が増減しないことが保証されている場合に限り、配列を選択してパフォーマンスを最適化します。
スライスの出番は、主に「関数の引数」です。例えば、リストの中身を表示する関数を作る際、引数を &Vec<i32> にしてしまうと、ベクタしか渡せなくなります。しかし、引数を &[i32] (スライス)にすれば、ベクタの一部も配列の一部も渡せるようになり、再利用性が劇的に向上します。Rustの標準ライブラリの多くがこのパターンを採用しています。
// スライスを引数に取ることで、配列もベクタも受け取れる汎用的な関数
fn print_total(values: &[i32]) {
let mut sum = 0;
for &val in values {
sum += val;
}
println!("合計値: {}", sum);
}
fn main() {
let my_array = [10, 20, 30];
let my_vector = vec![1, 2, 3, 4, 5];
// 配列をスライスとして渡す
print_total(&my_array);
// ベクタをスライスとして渡す
print_total(&my_vector);
}
6. メモリ安全性を支える境界チェックの仕組み
Rustが「メモリ安全な言語」と呼ばれる理由の一つに、配列やベクタへのアクセス時の**境界チェック**があります。C言語などの古い言語では、配列の範囲外(例えば要素数5なのに10番目)にアクセスしようとすると、隣接するメモリを破壊したり、セキュリティホールになったりすることがありました。これを「バッファオーバーフロー」と呼びます。
Rustでは、実行時に必ず「指定したインデックスが範囲内にあるか」をチェックします。もし範囲外にアクセスしようとした場合は、プログラムが安全に停止(パニック)し、不正なメモリアクセスを未然に防ぎます。これにより、初心者がうっかり間違ったインデックスを指定しても、システム全体を壊すような致命的なバグにはなりにくいのです。パフォーマンスを重視しつつも、安全性を決して妥協しないRustの設計思想がここにも現れています。
7. イテレータと組み合わせて効率的に処理する
配列やベクタの要素を一つずつ処理する場合、Rustでは for ループと iter() メソッドを組み合わせるのが一般的です。これは「イテレータ(Iterator)」と呼ばれる仕組みで、インデックスを自分で管理する必要がないため、境界外アクセスのリスクをゼロにできます。また、関数型プログラミングのような map や filter といった強力なメソッドも利用可能になります。
例えば、数値のリストから偶数だけを取り出して2倍にする、といった処理もイテレータを使えば数行で書けます。イテレータは内部的に最適化されており、手動で while ループを書くよりも高速に動作する場合が多いです。Rustのコレクション操作に慣れてきたら、ぜひイテレータを活用して、より宣言的で美しいコードを目指してみてください。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// 偶数だけを選んで、それぞれを10倍にした新しいベクタを作る
let results: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 10)
.collect();
println!("加工後のデータ: {:?}", results);
}
8. 文字列スライスstrとStringの関係
実は、私たちが普段使っている「文字列」も、今回学んだ概念と深く関わっています。Rustの String 型は、実は内部的には Vec<u8> (バイトのベクタ)として実装されており、ヒープ領域に保存される動的な文字列です。一方、文字列リテラル &str は、今回学んだスライスの文字列版です。
つまり、String はデータの所有者であり、&str はその一部を指し示す参照なのです。ベクタとスライスの関係を知ることは、Rust最大の難関と言われる「文字列の扱い」を理解するための第一歩でもあります。配列・ベクタ・スライスの基本をマスターすれば、Rust全体の理解度が飛躍的に高まることは間違いありません。一つずつコードを書いて、動きを確認しながら進めていきましょう。
今回の記事では、Rustの基礎である配列、ベクタ、スライスの違いについて詳しく解説しました。プログラムの要件に合わせて、これらを適切に選択できるようになれば、Rustエンジニアとしてのレベルが一段階アップします。次は、これらのデータをさらに便利に扱う「所有権の移動(ムーブ)」や「借用」についても学んでみてくださいね!