【Rust入門】Vec 型の基本について分かりやすく解説

Rust でデータシーケンスを扱うための Vec
型の基本を初心者にも分かりやすく解説します。
Vec<T>
型
Rust には、同じ型のデータをまとめて管理するための「コレクション(Collections)」が用意されています。コレクションは、実行中に要素数を変更できる柔軟なデータ構造です。
この記事では、コレクション型の代表例である Vec<T>
型について分かりやすく解説します。
Vec<T>
型とは
Vec<T>
型は、任意の型(T
)のデータシーケンスを扱うためのデータ型です。メモリのヒープ領域に格納するため、プログラム実行時のデータ長の変更や大きなサイズのデータの扱いに適しています。
同じ型の値をまとめて管理する方法として、代表的なものに配列があります。ただし、配列はコンパイル時にサイズが決定するためにデータの長さをプログラム実行中に変更することができません。また、配列は通常スタック領域に確保されるため極端に大きなサイズの場合は、コンパイルエラーや実行時のスタックオーバーフローの原因となります。
メモリ領域の中には「テキスト領域」「静的領域」「スタック領域」「ヒープ領域」があります。
- テキスト領域:プログラムの実行コードが格納されます。
- 静的領域:
static
変数など、プログラム全体を通じて使う値を格納します。 - スタック領域:基本型(
i32
など)のサイズが決まった小さな値が置かれます。 - ヒープ領域:
String
やVec
のようにサイズが実行時に決まるデータが使用する領域です。
Vec の生成方法
vec
マクロを使用する
Vec
型を生成するには、以下のように vec
マクロを使用し、[]
内に確保する要素を列挙します。以降の例では i32
型の例で紹介しますが、他の任意の型で同様に使用できます。
fn main() { // Vec マクロを利用して生成する let v = vec![1, 2, 3, 4, 5]; println!("{:?}", v); }
【実行結果】 [1, 2, 3, 4, 5]
なお、Vec<T>
は println
で {}
は使用できませんが、T
が Debug
トレイトを実装していれば、 {:?}
を使用して表示することができます。
配列同様に [値; 回数]
を指定することで同じ要素を繰り返す Vec
型の生成も可能です。
fn main() { // 同じ要素を繰り返す Vec を生成する let v = vec![0; 5]; println!("{:?}", v); }
【実行結果】 [0, 0, 0, 0, 0]
関連関数 Vec::new()
を使用する
vec
マクロ以外の生成方法としては、関連関数の Vec::new
関数を使用する方法があります。
fn main() { // 関連関数 new で Vec を生成する let mut v: Vec<i32> = Vec::new(); // 要素を追加する v.push(1); v.push(2); v.push(3); println!("{:?}", v); }
【実行結果】 [1, 2, 3]
new
関数では空の Vec
が生成されるため、上記のように push
等のメソッドを使用して要素を追加していくことができます。代表的な各種メソッドについては以降で紹介していきます。
Vec
要素の取得・追加・削除・結合
Vec
型では、要素の取得・追加・削除・結合では、各種メソッドが用意されています。
要素の取得
Vec の要素を取得するには、[]
で要素位置を指定するか、get
メソッドで取得できます。
fn main () { let v = vec![10, 20, 30]; println!("{}", v[0]); println!("{:?}", v.get(2)); println!("{:?}", v.get(100)); // 以下のように範囲外を指定すると panic となる // println!("{}", v[100]); }
【実行結果】 10 Some(30) None
[]
での値の取得では、範囲外の位置を指定してしまうと実行時に panic
のエラーとなるので注意してください。一方、get
メソッドの返却値は、Option
型なので、値がある場合は Some
、範囲外の場合は None
となり安全に処理が可能です。
要素の追加
Vec
への要素の追加では、push
、insert
メソッドを使用します。
push
メソッド
push
では、以下のように指定した値を末尾に追加します。
fn main() { let mut v1 = vec![10, 20]; // 要素を 1 要素追加する v1.push(30); println!("{:?}", v1); }
【実行結果】 [10, 20, 30]
insert
メソッド
insert
では、以下のように指定した位置に要素を追加します。範囲外の位置を指定してしまうと実行時に panic
のエラーとなるので注意してください。
fn main() { let mut v1 = vec![10, 20, 30]; // 要素を指定位置に追加する v1.insert(1, 50); println!("{:?}", v1); // 以下のように範囲外を指定すると panic となる // v1.insert(10, 50); }
【実行結果】 [10, 50, 20, 30]
要素の削除
Vec
の要素を削除するには、pop
、remove
、clear
メソッドを使用します。
pop
メソッド
pop
では、以下のように末尾の 1 要素を削除し、Option
型で返却します。
fn main() { let mut v = vec![10, 20, 30, 40, 50]; // 末尾の要素を削除する let last = v.pop(); println!("{:?}", v); println!("{:?}", last); }
【実行結果】 [10, 20, 30, 40] Some(50)
返却値は、Option
型であり、値がある場合は Some<T>
、空の Vec
でも None
が返ります。これにより安全に処理をすることが可能です。
上記のような基本型(i32
等の Copy
トレイトを実装している型)の場合は、取り出した変数に値がコピーされますが、要素が String
や Vec
のように所有権を持つ型の場合、受け取った変数に所有権が移動(上記例では last
変数)し、スコープを抜ける際に drop
により解放されます。なお、戻り値を無視した場合は、その場で drop
により解放されます。
drop
は、型がスコープを抜ける時に実行する処理の振る舞いを定義する Drop
トレイトの実装で String
や Vec
といった所有権を持つ型でメモリ解放の処理を実装しています。
remove
メソッド
remove
メソッドでは、指定位置の要素を削除し、残りの要素を前に詰めます。
fn main() { let mut v = vec![10, 20, 30, 40, 50]; // 指定位置を削除 let removed = v.remove(1); println!("{}", removed); // 以下のように範囲外を指定すると panic となる // let removed = v.remove(10); }
【実行結果】 [10, 30, 40, 50] 20
remove
の返却値は pop
とは違い Option
型ではないため、範囲外の位置を指定してしまうと実行時に panic
のエラーとなるので注意してください。変数に削除した値を受け取る際の drop
の挙動は、 pop
の場合と同様です。
clear
メソッド
Vec
の要素を全て削除して空にする場合は、clear
メソッドを使用します。
fn main() { let mut v = vec![10, 20, 30, 40, 50]; // 要素を空にする v.clear(); println!("{:?}", v); }
【実行結果】 []
各要素が所有権を持つ型の場合、その場で drop
によりメモリ解放されます。
Vec の結合
Vec
に別の Vec
を結合する場合には、append
、extend
、concat
を使用することができます。
append
メソッド
append
では、引数に Vec
の可変参照を渡すことで結合します。
fn main() { let mut v1 = vec![10, 20, 30]; let mut v2 = vec![40, 50]; v1.append(&mut v2); println!("{:?}", v1); println!("{:?}", v2); }
【実行結果】 [10, 20, 30, 40, 50] []
append
メソッドは、引数に渡した Vec
の要素を全て結合し、結合元の Vec
は空になります。可変参照で渡すため、Vec
自体の所有権は失われず、空のまま使い続けることが可能です。
Vec
の要素が基本型(i32
等の Copy
トレイトを実装している型)の場合は、要素はコピーされますが、String
や Vec
のように所有権を持つ型が要素の場合、各要素の所有権が移動します。
extend
メソッド
extend
では、以下のように引数に指定した Vec
を結合します。extend
メソッドの引数には、Vec
型やその参照、または配列を渡すことができます。
fn main() { let mut v1 = vec![10, 20, 30]; let v2 = vec![40, 50]; // 所有権を移動しない v1.extend(&v2); println!("{:?}", v1); println!("{:?}", v2); // 所有権を移動する v1.extend(v2); println!("{:?}", v1); // 所有権が移動するため、以下はコンパイルエラーになる // println!("{:?}", v2); // 配列でも追加できる let arr = [60, 70, 80]; v1.extend(arr); println!("{:?}", v1); }
【実行結果】 [10, 20, 30, 40, 50] [40, 50] [10, 20, 30, 40, 50, 40, 50] [10, 20, 30, 40, 50, 40, 50, 60, 70, 80]
参照を渡す場合は、その後も元の Vec
変数は使用できます。Vec
の要素が基本型(i32
等の Copy
トレイトを実装している型)では要素はコピーされますが、String
や Vec
のように所有権を持つ型が要素の場合は、clone
が呼ばれて要素がコピーされます。
変数そのものを渡す場合は、Vec
の所有権が移動するので元の Vec
変数は使用できなくなります。要素が String
や Vec
のように所有権を持つ型の場合は、要素の所有権も移動します。
concat
メソッド
concat
では、複数の Vec
をもとにして新しい Vec
を作成できます。 Vec
の変数を要素に持つ Vec<Vec<T>>
を生成しておき concat
メソッドを呼び出します。
fn main() { let v1 = vec![10, 20, 30]; let v2 = vec![40, 50]; let v3 = vec![60, 70, 80, 90, 100]; let v_concat = vec![v1, v2, v3].concat(); println!("{:?}", v_concat); // 所有権は移動しているので以下はコンパイルエラーになる // println!("{:?}", v1); // println!("{:?}", v2); // println!("{:?}", v3); }
【実行結果】 [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
concat
を使う場合、一度別のメモリ領域を確保してから結合するため、大きなサイズの Vec
を結合する際にはメモリ使用量に注意してください。
繰り返し処理(for
)での利用方法
Vec
のように複数の要素を持つようなデータは繰り返し処理(for
)で頻繁に利用されます。Rust の繰り返しでは、イテレータを取得して動作するため、C/C++ や Java 等の for
文とは少し記載の方法は異なります。他言語でいうと Python と同じような構文です。
参照のみの繰り返し処理
Vec
型の要素を1つずつ取り出して参照するだけの繰り返し処理する場合には、以下のように不変参照を使って for
を使用します。
fn main() { let v = vec![10, 20, 30, 40, 50]; // for による繰り返し処理 (不変参照) for x in &v { println!("{}", x); } }
【実行結果】 10 20 30 40 50
上記は、Vec
型の v
の先頭から 1 要素ずつを取得し x
に代入して繰り返し処理が進みます。
値の変更を伴う繰り返し処理
Vec
型の要素を繰り返し処理の中で変更する場合には、以下のように可変参照を使います。
fn main() { let mut v = vec![10, 20, 30, 40, 50]; // for による繰り返し処理 (可変参照) for x in &mut v { println!("{}", x); *x += 1; } println!("{:?}", v); }
【実行結果】 10 20 30 40 50 [11, 21, 31, 41, 51]
上記例のようにすることで、*x で要素を変更することが可能になります。
所有権の移動を伴う繰り返し処理
Vec
型の所有権を移動して繰り返し処理をする場合には、以下のように in
の部分に Vec
の変数を直接指定します。
fn main() { let v = vec![10, 20, 30, 40, 50]; // 所有権を移動する for mut x in v { x += 1; println!("{}", x); } // 所有権が移動しているのでコンパイルエラーになる。 // println!("{:?}", v); }
【実行結果】 11 21 31 41 51
所有権を移動する繰り返し処理をした場合には、繰り返し後には変数を使うことができなくなります。上記では、ループ内で値が変更できるように mut x
として受け取っていますが、参照だけであれば mut
なしで「for x in v
」とすることもできます。
この方法は、ループ後に元の Vec
を使わない場合や大きなサイズの Vec
のためループ終了にあわせてメモリを解放したい場合などに利用できます。
まとめ
Rust でデータシーケンスを扱うための Vec
型の基本を解説しました。Vec<T>
は、同じ型(T
)の複数の値をヒープ領域に柔軟に格納できるコレクション型です。配列と異なり、実行時に要素数を変更できます。
Vec
の生成方法(vec![]
マクロや Vec::new()
)や、以下の各種メソッドの使い方を紹介しました。
- 取得:
[]
(範囲外で panic)やget
(Option 型で安全) - 追加:
push
(末尾)、insert
(指定位置) - 削除:
pop
(末尾)、remove
(指定位置)、clear
(全削除) - 結合:
append
、extend
、concat
また、for
での繰り返し処理の基本についても説明しました。
- 参照のみ:
for x in &vec
- 値の変更:
for x in &mut vec
- 所有権移動:
for x in vec
Vec
は Rust プログラミングでも頻繁に使用する重要なコレクションの型なので、使い方の基本をしっかりと理解しましょう。