Rust入門

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

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

Rust でデータシーケンスを扱うための Vec 型の基本を初心者にも分かりやすく解説します。

Vec<T>

Rust には、同じ型のデータをまとめて管理するための「コレクション(Collections)」が用意されています。コレクションは、実行中に要素数を変更できる柔軟なデータ構造です。

この記事では、コレクション型の代表例である Vec<T> 型について分かりやすく解説します。

Vec<T> 型とは

Vec<T>は、任意の型(T)のデータシーケンスを扱うためのデータ型です。メモリのヒープ領域に格納するため、プログラム実行時のデータ長の変更や大きなサイズのデータの扱いに適しています。

同じ型の値をまとめて管理する方法として、代表的なものに配列があります。ただし、配列はコンパイル時にサイズが決定するためにデータの長さをプログラム実行中に変更することができません。また、配列は通常スタック領域に確保されるため極端に大きなサイズの場合は、コンパイルエラーや実行時のスタックオーバーフローの原因となります。

配列については以下で説明しているので参考にしてください。

【Rust入門】基本型を分かりやすく解説(スカラー型と複合型)

メモリ領域

メモリ領域の中には「テキスト領域」「静的領域」「スタック領域」「ヒープ領域」があります。

  • テキスト領域:プログラムの実行コードが格納されます。
  • 静的領域: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{} は使用できませんが、TDebug トレイトを実装していれば、 {:?} を使用して表示することができます。

配列同様に [値; 回数] を指定することで同じ要素を繰り返す 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 への要素の追加では、pushinsert メソッドを使用します。

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 の要素を削除するには、popremoveclear メソッドを使用します。

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 トレイトを実装している型)の場合は、取り出した変数に値がコピーされますが、要素が StringVec のように所有権を持つ型の場合、受け取った変数に所有権が移動(上記例では last 変数)し、スコープを抜ける際に drop により解放されます。なお、戻り値を無視した場合は、その場で drop により解放されます。

drop は、型がスコープを抜ける時に実行する処理の振る舞いを定義する Drop トレイトの実装で StringVec といった所有権を持つ型でメモリ解放の処理を実装しています。

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 を結合する場合には、appendextendconcat を使用することができます。

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 トレイトを実装している型)の場合は、要素はコピーされますが、StringVec のように所有権を持つ型が要素の場合、各要素の所有権が移動します。

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 トレイトを実装している型)では要素はコピーされますが、StringVec のように所有権を持つ型が要素の場合は、clone が呼ばれて要素がコピーされます。

変数そのものを渡す場合は、Vec の所有権が移動するので元の Vec 変数は使用できなくなります。要素が StringVec のように所有権を持つ型の場合は、要素の所有権も移動します。

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(全削除)
  • 結合:appendextendconcat

また、for での繰り返し処理の基本についても説明しました。

  • 参照のみ:for x in &vec
  • 値の変更:for x in &mut vec
  • 所有権移動:for x in vec

Vec は Rust プログラミングでも頻繁に使用する重要なコレクションの型なので、使い方の基本をしっかりと理解しましょう。

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました