ベクタで値のリストを保持する
最初に見るコレクション型はVec<T>です。ベクタとも呼ばれます。
ベクタを使用することで、複数の値をメモリ上で互いに隣り合うように単一のデータ構造に保持することができます。
ベクタは同じ型の値のみ保持することができます。
テキストファイルの各行や、ショッピングカートのアイテムの価格などのような、要素のリストがある場合にベクタは有用です。
新しいベクタを生成する
空のベクタを新たに作成するには、リスト8-1に示すようにVec::new関数を呼びます。
fn main() { let v: Vec<i32> = Vec::new(); }
リスト8-1:i32型の値を保持する新しい空のベクタを作成する
ここで型注釈を付けていることに注目してください。
このベクタには何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか分かりません。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています; ジェネリクスを使用する自身の型をどう使用するかついては第10章で解説します。
今のところは、標準ライブラリで提供されるVec<T>型は、どんな型でも保持できると理解しておいてください。
特定の型を保持するベクタを作成するときには、その型を山かっこ内に指定すればよいです。
リスト8-1では、コンパイラにvのVec<T>はi32型の要素を保持すると指示していたのです。
Vec<T>を作成する場合は初期値を持たせることのほうが多いでしょう。
その場合コンパイラは保持したい値の型を推論するでしょうから、このように型注釈を付ける必要はほとんどありません。
Rustにはvec!という便利なマクロが用意されていて、これは与えた値を保持する新しいベクタを作成します。
リスト8-2では、1、2、3という値を持つ新しいVec<i32>を作成しています。
整数の型がi32になっているのは、3章の「データ型」節で学んだように、これがデフォルトの整数型だからです。
fn main() { let v = vec![1, 2, 3]; }
リスト8-2: 値を含む新しいベクタを作成する
初期値のi32値を与えたので、コンパイラは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); }
リスト8-3:pushメソッドを使用してベクタに値を追加する
第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]; // "3つ目の要素は{third}です" println!("The third element is {third}"); let third: Option<&i32> = v.get(2); match third { // "3つ目の要素は{third}です" Some(third) => println!("The third element is {third}"), // "3つ目の要素はありません。" None => println!("There is no third element."), } }
リスト8-4:添え字記法かgetメソッドを使用してベクタの要素にアクセスする
ここでいくつか注意があります。
ベクタは0から始まる番号で添え字アクセスされるので、3番目の要素を得るのに2という添え字の値を使用しています。
&と[]を使用することで、その添え字の値にある要素への参照が得られます。
getメソッドに引数として添え字を渡して使用すると、Option<&T>が得られ、これはmatchで使用することができます。
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); }
リスト8-5:5つの要素を含むベクタの添え字100の要素にアクセスしようとする
このコードを走らせると、最初の[]メソッドはプログラムをパニックさせます。
なぜなら存在しない要素を参照しているからです。
このメソッドは、ベクタの終端を超えて要素にアクセスしようとしたときにプログラムをクラッシュさせたい場合に最適です。
getメソッドにベクタ外の添え字を渡すと、パニックすることなくNoneを返します。
普通の状況でもベクタの範囲外にアクセスする可能性があるなら、このメソッドを使用することになるでしょう。
その場合、第6章で説明したように、コードはSome(&element)かNoneを扱うロジックを持つことになります。
例えば、誰かが入力した数値が添え字になるかもしれません。
もし誤って大きすぎる値を入力し、プログラムがNone値を得たなら、いまベクタに何要素あるかをユーザに教え、正しい値を再入力してもらうこともできます。
その方が、ただのタイプミスでプログラムをクラッシュさせるより、ユーザに優しいといえそうです。
プログラムに有効な参照がある場合、借用チェッカー (borrow checker) は、(第4章で解説しましたが)所有権と借用規則を強制し、ベクタの中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。
同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。
このルールはリスト8-6でも適用されています。
リスト8-6ではベクタの最初の要素への不変参照を保持しつつ、終端に要素を追加しようとしています。
このプログラムは、関数内のここ以降で、この要素(訳注:firstのこと)を参照しようとすると失敗します。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
// "最初の要素は: {first}"
println!("The first element is: {first}");
}
リスト8-6:要素への参照を保持しつつ、ベクタに要素を追加しようとする
このコードをコンパイルすると、こんなエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
--> 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でforループを使い、i32のベクタの各要素に対する不変な参照を得て、それらを表示する方法を示します。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } }
リスト8-7:forループで要素を走査し、ベクタの各要素を表示する
また、全要素に変更を加えるために、可変なベクタの各要素への可変な参照を走査することもできます。
リスト8-9のforループでは各要素に50を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
リスト8-8:ベクタの要素への可変な参照を走査する
可変参照が参照している値を変更するには、+=演算子を使用する前に、*参照外し演算子を使用してiの値にたどり着かないといけません。
参照外し演算子については、第15章の「参照外し演算子で値までポインタを追いかける」節でより詳しく扱います。
借用チェッカーの規則のおかげで、ベクタを走査するのは不変であっても可変であっても安全です。
もしリスト8-7やリスト8-8のforループ本体の中で要素を挿入または削除しようとすると、リスト8-6のコードで発生したのと同じようなコンパイルエラーになるでしょう。
forループがベクタへの参照をつかんでいるために、それと同時にベクタ全体の変更が起こらないようになっています。
Enumを使って複数の型を保持する
ベクタは同じ型の値しか保持できません。これは不便なこともあります。 異なる型の要素を保持する必要のあるユースケースは必ず存在します。 幸運なことに、enumの列挙子は同じenumの型の中に定義されるので、異なる型の要素を表現する単一の型が必要になったら、enumを定義して使用すればよいのです!
例えば、スプレッドシートのある行から値を得ることを考えます。 ここで、その行の中の列には、整数を含むもの、浮動小数点数を含むもの、文字列を含むものがあるとします。 列挙子ごとに異なる値の型を保持する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), ]; }
リスト8-9:enumを定義して、一つのベクタに異なる型の値を保持する
個々の要素を格納するのにヒープ上で必要となるメモリの量を正確に把握するめに、Rustコンパイラはコンパイル時にベクタに入る型を知る必要があります。
また、このベクタではどんな型が許容されるのか明示しなくてはなりません。
もしRustが、ベクタにどんな型でも保持できることを許していたら、ベクタの要素に対して行われる処理に対して、いくつかの型がエラーを引き起こすかもしれません。
enumに加えてmatch式を使うことで、第6章で説明したとおり、あらゆるケースが処理できることを、Rustがコンパイル時に保証することになります。
プログラムが実行時に取得し、ベクタに格納し得る全ての型を網羅できない場合には、このenumを使ったテクニックはうまくいかないでしょう。 代わりにトレイトオブジェクトを使用できます。 こちらは第17章で取り上げます。
これまでにベクタの代表的な使い方をいくつか紹介しました。
標準ライブラリでVec<T>に定義されている多くの有益なメソッドについて、APIドキュメントを必ず確認するようにしてください。
例えば、pushに加えて、popというメソッドがあり、これは最後の要素を削除して返します。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-10に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
リスト8-10:ベクタとその要素がドロップされる箇所を示す
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 借用チェッカーは、ベクタの内容へのいかなる参照も、ベクタ自体が有効な間のみ使用されることを保証します。
それでは次のコレクション型であるStringに移りましょう!