ベクターで値のリストを格納する
最初に見るコレクション型は、ベクターとも呼ばれる Vec<T> です。
ベクターを使うと、複数の値を 1 つのデータ構造に格納でき、それらの値は
メモリ上で互いに隣り合って配置されます。ベクターに格納できるのは、
同じ型の値だけです。ファイル内のテキストの行やショッピングカート内の
商品の価格のように、項目のリストを扱うときに便利です。
新しいベクターを作成する
新しい空のベクターを作成するには、リスト 8-1 に示すように
Vec::new 関数を呼び出します。
fn main() {
let v: Vec<i32> = Vec::new();
}
ここでは型注釈を追加していることに注目してください。このベクターには
まだ何の値も挿入していないため、Rust はどの種類の要素を格納したいのかを
知ることができません。これは重要な点です。ベクターはジェネリクスを使って
実装されています。独自の型でジェネリクスを使う方法については、第 10 章で
扱います。今のところは、標準ライブラリが提供する Vec<T> 型は任意の型を
保持できるということを知っておいてください。特定の型を保持するベクターを
作成する場合、その型を山かっこ内に指定できます。リスト 8-1 では、v の
Vec<T> が i32 型の要素を保持することを Rust に伝えています。
より一般的には、初期値付きで Vec<T> を作成し、Rust が格納したい値の型を
推論してくれるため、この型注釈が必要になることはあまりありません。Rust は
便利な vec! マクロも提供しており、渡した値を保持する新しいベクターを
作成できます。リスト 8-2 では、1、2、3 を保持する新しい
Vec<i32> を作成しています。整数型が i32 なのは、第 3 章の
「データ型」 節で説明したとおり、それが
デフォルトの整数型だからです。
fn main() {
let v = vec![1, 2, 3];
}
初期値として i32 の値を与えているので、Rust は v の型が Vec<i32> で
あると推論でき、型注釈は不要です。次に、ベクターを変更する方法を見ていきます。
ベクターを更新する
ベクターを作成してから要素を追加するには、リスト 8-3 に示すように
push メソッドを使います。
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
どの変数でもそうであるように、その値を変更できるようにしたい場合は、
第 3 章で説明した mut キーワードを使って可変にする必要があります。
中に入れる数値はすべて i32 型であり、Rust はデータからこれを推論するため、
Vec<i32> の注釈は必要ありません。
ベクターの要素を読み取る
ベクターに格納された値を参照する方法は 2 つあります。インデックスを使う方法と、
get メソッドを使う方法です。次の例では、分かりやすさのために、これらの
関数から返される値の型に注釈を付けています。
リスト 8-4 は、インデックス構文と get メソッドを使って、ベクター内の値に
アクセスする両方の方法を示しています。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
ここでいくつかの詳細に注意してください。2 というインデックス値を使って
3 番目の要素を取得していますが、これはベクターが 0 から始まる番号で
インデックス付けされるためです。& と [] を使うと、インデックス位置の
要素への参照が得られます。インデックスを引数として get メソッドを使うと、
match と組み合わせて使える Option<&T> が得られます。
Rust が要素を参照する方法をこの 2 つ提供しているのは、存在する要素の範囲外の インデックスを使おうとしたときに、プログラムをどのように振る舞わせたいかを 選べるようにするためです。例として、5 つの要素を持つベクターがあり、その後で それぞれの手法を使ってインデックス 100 の要素にアクセスしようとすると何が 起こるかを、リスト 8-5 で見てみましょう。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
このコードを実行すると、最初の [] による方法では、存在しない要素を
参照しているため、プログラムがパニックを起こします。この方法は、ベクターの
末尾を超える要素にアクセスしようとする試みがあった場合に、プログラムを
クラッシュさせたいときに最適です。
get メソッドにベクターの範囲外のインデックスを渡すと、パニックを起こさずに
None を返します。このメソッドは、通常の状況でもベクターの範囲を超えた
要素へのアクセスが時折起こり得る場合に使います。その場合、コードには
第 6 章で説明したように、Some(&element) と None のどちらにも対応する
ロジックが必要になります。たとえば、インデックスが人が入力した数値から
来ることがあります。もし誤って大きすぎる数値を入力して、プログラムが
None を受け取ったなら、現在のベクターにいくつ項目があるのかをユーザーに
伝え、有効な値をもう一度入力する機会を与えられます。それは、タイプミスが
原因でプログラムをクラッシュさせるよりも、ユーザーフレンドリーです。
プログラムが有効な参照を持っている場合、借用チェッカーは所有権と借用の ルール(第 4 章で扱いました)を適用し、この参照およびベクターの内容に対する ほかの参照が有効なままであることを保証します。同じスコープ内では可変参照と 不変参照を同時に持てない、というルールを思い出してください。そのルールは リスト 8-6 にも適用されます。ここでは、ベクターの最初の要素への不変参照を 保持したまま、末尾に要素を追加しようとしています。このプログラムは、後で 関数内でその要素を参照しようとすると動作しません。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
このコードをコンパイルすると、次のエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
リスト 8-6 のコードは、一見すると動作してよさそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクターの末尾での変更を気にするのでしょうか。 このエラーは、ベクターの動作の仕組みによるものです。ベクターは値をメモリ上で 互いに隣り合わせに配置するため、ベクターの末尾に新しい要素を追加するとき、 現在ベクターが格納されている場所にすべての要素を隣り合わせで置くのに十分な 空きがない場合には、新しいメモリを確保して古い要素を新しい領域へコピーする 必要が生じることがあります。その場合、最初の要素への参照は解放済みの メモリを指すことになります。借用のルールは、プログラムがそのような状態に 陥るのを防ぎます。
注:
Vec<T>型の実装の詳細については、「The Rustonomicon」 を参照してください。
ベクター内の値を反復処理する
ベクタの各要素に順番にアクセスするには、インデックスを使って1つずつ
アクセスするのではなく、すべての要素を反復処理します。リスト8-7は、
i32 値のベクタの各要素への不変参照を for ループで取得し、それらを
出力する方法を示しています。
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
可変ベクタの各要素への可変参照を反復処理して、すべての要素に変更を
加えることもできます。リスト8-8の for ループは、各要素に 50 を
加えます。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
可変参照が参照している値を変更するには、+= 演算子を使う前に *
参照外し演算子を使って i の中の値にたどり着かなければなりません。
参照外し演算子については、第15章の
「参照が指す値をたどる」 節で詳しく説明します。
不変でも可変でも、ベクタの反復処理は借用チェッカーの規則のおかげで
安全です。もしリスト8-7やリスト8-8の for ループ本体で要素を挿入したり
削除したりしようとすると、リスト8-6のコードで得たものと同様の
コンパイルエラーになります。for ループが保持しているベクタへの参照が、
ベクタ全体の同時変更を防いでいるのです。
複数の型を格納するために enum を使う
ベクタには同じ型の値しか格納できません。これは不便なことがあります。 異なる型の項目のリストを格納したいユースケースは確かに存在します。 幸い、enum のバリアントは同じ enum 型のもとで定義されるので、異なる型の 要素を1つの型で表現する必要があるときは、enum を定義して使えます!
たとえば、ある行のいくつかの列には整数、いくつかの列には浮動小数点数、 そしていくつかの列には文字列が入っているスプレッドシートの1行から値を 取得したいとします。異なる値型を保持するバリアントを持つ enum を定義 すれば、すべての enum バリアントは同じ型、すなわちその enum の型だと みなされます。すると、その enum を保持するベクタを作成でき、結果として 異なる型を格納できます。これをリスト8-9で示しています。
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust は、各要素を格納するのにヒープ上でどれだけのメモリが必要になるかを
正確に把握するため、コンパイル時にベクタ内にどの型が入るのかを知って
いる必要があります。また、このベクタでどの型を許可するのかを明示しなけ
ればなりません。もし Rust がベクタにあらゆる型を保持できるようにして
いたら、そのうちの1つ以上の型が、ベクタの要素に対して行う操作でエラーを
引き起こす可能性があります。enum と match 式を組み合わせて使えば、
第6章で説明したように、考えられるすべてのケースが処理されていることを
Rust がコンパイル時に保証してくれます。
実行時にプログラムがベクタに格納するために受け取る型の網羅的な集合が 分からない場合、enum の手法は使えません。代わりに、トレイトオブジェクトを 使えます。これについては第18章で扱います。
ベクタの最も一般的な使い方のいくつかを見てきたので、標準ライブラリが
Vec<T> に定義している数多くの便利なメソッドについては、
API ドキュメント をぜひ確認してください。
たとえば、push に加えて、pop メソッドは最後の要素を取り除いて返します。
ベクタをドロップするとその要素もドロップされる
ほかのあらゆる struct と同様に、ベクタはスコープを抜けると解放されます。
これはリスト8-10で注釈付きで示しています。
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
ベクタがドロップされると、その中身もすべてドロップされるため、保持して いる整数もクリーンアップされます。借用チェッカーは、ベクタの中身への 参照が、ベクタ自体が有効な間だけ使われることを保証します。
次は、次のコレクション型である String に進みましょう!