RustのVec(ベクタ)を完全攻略!可変長配列の基本からメモリ管理まで初心者向けに解説
生徒
「Rustでデータをまとめたい時、普通の配列だとサイズが決まっていて不便なんです。後から要素を増やしたり減らしたりする方法はありませんか?」
先生
「その悩み、Vec(ベクタ)を使えば解決できますよ!Rustのベクタは『可変長配列』とも呼ばれていて、プログラムの実行中に自由にサイズを変えることができるんです。」
生徒
「自由にサイズを変えられるのは便利ですね!でも、普通の配列と何が違うんですか?」
先生
「大きな違いはメモリの保存場所です。ベクタは『ヒープ領域』という場所にデータを保存するので、柔軟な操作が可能です。具体的にどう使うのか、一緒に見ていきましょう!」
1. RustのVec(ベクタ)とは?可変長配列の基本
RustにおけるVec<T>(ベクタ)は、同じ型の値を複数格納できる動的なリスト構造です。プログラミングをしていると、最初からデータの個数が分かっていることばかりではありません。ユーザーが入力した情報の数だけデータを保存したい、あるいは検索結果の件数に応じてリストを作りたいといった場面が多々あります。
通常の配列(Array)は「固定長」であり、コンパイル時にサイズが確定している必要があります。これに対してベクタは、実行時に要素を付け加えたり、削除したりすることが可能な「可変長」の特性を持っています。この柔軟性こそが、実務のアプリケーション開発でベクタが最も多用される理由です。型名の <T> はジェネリクスと呼ばれ、整数や文字列など、あらゆる型を扱えることを意味しています。
2. ベクタの作成と初期化のバリエーション
ベクタを新しく作成する方法は主に二つあります。一つは Vec::new() 関数を使って空のベクタを作成する方法、もう一つは vec! マクロを使って初期値を入れた状態で作成する方法です。
空のベクタを作る場合は、後から要素を追加することを想定しているため、変数に mut(ミュータブル)キーワードを付けて宣言する必要があります。一方で、最初から中身が決まっている場合は vec! マクロを使うと配列のような感覚で直感的に記述でき、非常にコードが読みやすくなります。用途に応じてこれらを使い分けるのがRust流の書き方です。
fn main() {
// 空のベクタを作成(後で追加するため mut を付与)
let mut numbers: Vec<i32> = Vec::new();
// マクロを使って初期値ありで作成
let fruits = vec!["りんご", "みかん", "バナナ"];
println!("空ベクタの長さ: {}", numbers.len());
println!("果物リスト: {:?}", fruits);
}
3. 要素の追加と削除を自由に行う方法
ベクタの真骨頂は、中身を自由に入れ替えられることです。要素を末尾に追加するには push メソッドを使用します。逆に、末尾の要素を取り出して削除するには pop メソッドを使います。これらは非常に高速に動作するよう最適化されています。
注意点として、pop メソッドは削除した値を Option 型で返します。これは、もしベクタが空だった場合にエラー(パニック)を起こさず、「値がない」という状態を安全に扱うためのRust独自の工夫です。このように、ベクタの操作一つをとっても、Rustのメモリ安全へのこだわりが随所に現れています。
fn main() {
let mut stack = Vec::new();
// データの追加
stack.push(10);
stack.push(20);
stack.push(30);
println!("追加後の状態: {:?}", stack);
// 末尾からデータを取り出す
let last_item = stack.pop();
println!("取り出した値: {:?}", last_item);
println!("現在のベクタ: {:?}", stack);
}
4. ベクタとメモリ管理の関係(ヒープ領域)
なぜベクタはサイズを変更できるのでしょうか?その秘密はメモリの「ヒープ領域」にあります。通常の固定長配列は「スタック」と呼ばれる高速ですがサイズ制限の厳しい場所に置かれます。一方、ベクタの本体データは広大で自由な「ヒープ」に確保されます。
ベクタの構造体自体はスタックに置かれ、「ヒープのどこにデータがあるか」というポインタ、現在の「要素数(長さ)」、そして「容量(キャパシティ)」の3つの情報を持っています。要素が増えて現在の容量を超えそうになると、Rustは自動的により広いメモリ場所を探してデータを引っ越しさせます。この「再確保」という仕組みのおかげで、私たちはメモリの限界を気にせず要素を追加し続けることができるのです。
5. インデックスによるアクセスと境界チェック
ベクタ内の特定の要素にアクセスするには、配列と同じように [](ブラケット)とインデックス番号を使います。インデックスは0から始まります。例えば、3番目の要素にアクセスしたい場合は vec[2] と記述します。
ここでRustの初心者が気をつけなければならないのが「境界チェック」です。存在しないインデックス(例えば要素が3つしかないのに10番目など)を指定すると、Rustはプログラムを即座に停止させます。これを「パニック」と呼びます。もしパニックを避けたい場合は、get メソッドを使用することで、安全に値の有無を確認しながらアクセスすることが可能です。
fn main() {
let v = vec![100, 200, 300];
// 直接アクセス(範囲外だとパニックする)
println!("2番目の要素: {}", v[1]);
// getメソッドによる安全なアクセス
match v.get(5) {
Some(val) => println!("値が見つかりました: {}", val),
None => println!("指定した場所には値がありません。"),
}
}
6. ベクタの全要素をループで処理する
ベクタに格納された大量のデータを処理する場合、for ループが非常に便利です。Rustでは、ベクタの要素を「参照」として取り出すことで、データの所有権を移動させずに中身を読み取ることができます。
もし中身を書き換えたい場合は、&mut v のようにミュータブルな参照を使ってループを回します。イテレータ(Iterator)という仕組みが背後で動いており、これにより非常に効率的な繰り返し処理が実現されています。インデックスを手動で管理するよりも安全で、読みやすいコードになります。
fn main() {
let mut scores = vec![80, 50, 40];
// 全要素にボーナス加算
for score in &mut scores {
*score += 10;
}
// 全要素を表示
for (i, score) in scores.iter().enumerate() {
println!("{}人目のスコア: {}", i + 1, score);
}
}
7. キャパシティとパフォーマンスの最適化
ベクタには「長さ(len)」と「容量(capacity)」という二つの異なる概念があります。「長さ」は現在実際に入っている要素の数ですが、「容量」はメモリ上に確保されている合計のスペースを指します。
要素を追加するたびにメモリの再確保(引っ越し)が発生すると、動作が重くなってしまいます。そのため、もし最初から「だいたい100個くらい要素が入るな」と分かっている場合は、Vec::with_capacity(100) を使うのがプロの技です。あらかじめ広めの部屋を予約しておくことで、余計な引っ越し作業を減らし、パフォーマンスを劇的に向上させることができます。
8. ベクタからスライスへの変換
ベクタを関数の引数として渡す際、&Vec<T> ではなく &[T](スライス)として渡すのがRustの一般的なパターンです。スライスはベクタや配列の「一部を切り出した窓口」のようなもので、所有権を持たない参照です。
なぜスライスを使うのかというと、その方が関数の汎用性が高まるからです。スライスを引数にする関数は、ベクタだけでなく固定長配列も受け取れるようになります。Rustコンパイラは必要に応じてベクタを自動的にスライスへ変換(型強制)してくれるため、呼び出し側は特に意識することなく安全にデータを共有できます。
9. ベクタを使う際の所有権の注意点
Rustの最も特徴的なルールである「所有権」はベクタにも適用されます。例えば、ある関数にベクタをそのまま渡してしまうと、元の場所ではそのベクタを使えなくなってしまいます。これを「ムーブ(移動)」と呼びます。
多くの場合はベクタの内容を読み取るだけで十分なため、所有権を渡すのではなく「参照(借用)」を渡すようにしましょう。また、ベクタの中に文字列などの所有権を持つデータを入れている場合、ベクタがスコープを抜けて破棄されるタイミングで、中身のデータもすべて一斉に正しくメモリから解放されます。これがRustが「ガベージコレクションなしでメモリ安全」と言われる所以です。
10. 異なるデータ型をベクタで扱うには?
基本的に一つのベクタには同じ型のデータしか入れられません。しかし、どうしても「数値と文字列を混ぜてリスト化したい」という場面もあるでしょう。その場合、Rustでは Enum(列挙型)や Trait Object(トレイトオブジェクト)を活用します。
例えば、複数の型を包み込む列挙型を定義し、その列挙型のベクタを作ることで、擬似的に異なるデータを共存させることができます。このように、ベクタは単純なリストでありながら、Rustの他の機能と組み合わせることで非常に高度なデータ構造へと進化させることが可能です。まずは基本の Vec<T> を使いこなし、徐々に応用的な使い方にチャレンジしていきましょう!
Rustの学習において、ベクタを自由に操れるようになることは大きな一歩です。可変長という柔軟性を持ちつつ、厳格なメモリ管理と型システムによって守られているベクタは、安全で高速なプログラムを書くための強力な武器になります。ぜひ、この記事で紹介したサンプルコードを自分の環境で実行して、その挙動を確かめてみてくださいね!