ベクタで値のリストを保持する
最初に見るコレクション型はVec<T>
であり、これはベクタとしても知られています。
ベクタは単体のデータ構造でありながら複数の値を保持でき、それらの値をメモリ上に隣り合わせに並べます。
ベクタには同じ型の値しか保持できません。
要素のリストがある場合にベクタは有用です。
例えば、テキストファイルの各行とか、ショッピングカートのアイテムの価格などです。
新しいベクタを生成する
空のベクタを新たに作るには、リスト8-1に示すようにVec::new
関数を呼びます。
fn main() { let v: Vec<i32> = Vec::new(); }
ここで、型注釈を付けていることに注目してください。
なぜなら、このベクタに対して何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか推測できないからです。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています。
あなた自身の型でどうジェネリクスを使用するかついては第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]; }
初期値の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); }
第3章で説明したとおり、どんな変数でも、その値を変更したかったらmut
キーワードで可変にする必要があります。
中に配置する数値は全てi32
型であり、Rustはこのことをデータから推論するので、Vec<i32>
という注釈は不要です。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct
(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-4に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 これは一見単純そうですが、ベクタの要素に対する参照を使い始めると少し複雑になり得ます。 次はそれに挑戦しましょう!
ベクタの要素を読む
ベクタを生成し、更新し、破棄する方法がわかったので、次のステップでは中身を読む方法について学ぶのが良いでしょう。 ベクタに保持された値を参照する方法は2つあります。 これから示す例では、理解を助けるために、それらの関数からの戻り値型を注釈しています。
リスト8-5はベクタの値にアクセスする両方の方法として、添え字記法とget
メソッドが示されています。
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { // "3つ目の要素は{}です" Some(third) => println!("The third element is {}", third), // "3つ目の要素はありません。" None => println!("There is no third element."), } }
ここでは2つのことに注目してください。
1つ目は、3番目の要素を得るのに2
という添え字の値を使用していることです。
ベクタは番号で索引化されますが、その番号は0から始まります。
2つ目は、3番目の要素を得る2つの方法とは、&
と[]
を使用して参照を得るものと、get
メソッドに引数として添え字を渡してOption<&T>
を得るものだということです。
Rustのベクタには要素を参照する方法が2通りあるので、ベクタに含まれない要素の添え字を使おうとしたときのプログラムの振る舞いを選択できます。 例として、ベクタに5つ要素があるとして、添え字100の要素にアクセスを試みた場合、プログラムがどうなるのか確認しましょう。 リスト8-6に示します。
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
値を得たなら、いまベクタに何要素あるかをユーザに教え、正しい値を再入力してもらうこともできます。
その方が、ただのタイプミスでプログラムをクラッシュさせるより、ユーザに優しいといえそうです。
プログラムに有効な参照がある場合、借用チェッカー (borrow checker) は、(第4章で解説しましたが)所有権と借用規則を強制し、ベクタの中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。
同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。
このルールはリスト8-7でも適用されています。
リスト8-7ではベクタの最初の要素への不変参照を保持しつつ、終端に要素を追加しようとしています。
関数内のここ以降で、この要素(訳注:first
のこと)を参照しようとすると失敗します。
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
(エラー: 不変としても借用されているので、`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
| (その後、不変借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections`.
To learn more, run the command again with --verbose.
リスト8-7のコードは、一見動きそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクタの終端への変更を気にかける必要があるのでしょうか? このエラーはベクタが動作するしくみによるものです。 新たな要素をベクタの終端に追加するとき、いまベクタのある場所に全要素を隣り合わせに配置するだけのスペースがないなら、新しいメモリを割り当て、古い要素を新しいスペースにコピーする必要があります。 その場合、最初の要素を指す参照は、解放されたメモリを指すことになるでしょう。 借用規則がそのような状況に陥らないよう防いでくれるのです。
注釈:
Vec<T>
の実装に関する詳細については、“The Rustonomicon”を参照してください (訳注:日本語版はこちらです)。
ベクタ内の値を順に処理する
ベクタの要素に順番にアクセスしたいなら、添え字で1要素ごとにアクセスするのではなく、全要素を走査することができます。
リスト8-8でfor
ループを使い、i32
のベクタの各要素に対する不変な参照を得て、それらを表示する方法を示します。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
また、全要素に変更を加えるために、可変なベクタの各要素への可変な参照を走査することもできます。
リスト8-9のfor
ループでは各要素に50
を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
可変参照が参照している値を変更するには、+=
演算子を使用する前に、参照外し演算子(*
)を使用してi
の値にたどり着かないといけません。
参照外し演算子については、第15章の「参照外し演算子で値までポインタを追いかける」節でより詳しく扱います。
Enumを使って複数の型を保持する
この章の冒頭で、ベクタは同じ型の値しか保持できないと述べました。 これは不便なこともあります。 異なる型の要素を保持する必要のあるユースケースは必ず存在します。 幸運なことに、enumの列挙子は同じenumの型の中に定義されるので、ベクタに異なる型の要素を保持する必要が出たら、enumを定義して使用すればよいのです!
例えば、スプレッドシートのある行から値を得ることを考えます。 ここで、その行の中の列には、整数を含むもの、浮動小数点数を含むもの、文字列を含むものがあるとします。 列挙子ごとに異なる値の型を保持するenumが定義できます。 そして、このenumの列挙子は全て同じ型、つまりenumの型、と考えられるわけです。 ですから、そのenumを保持するベクタを作成でき、結果的に異なる型を保持できるようになるわけです。 リスト8-10でこれを実演しています。
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が、ベクタにどんな型でも保持できることを許していたら、ベクタの要素に対して行われる処理に対して、いくつかの型がエラーを引き起こすかもしれません。
enumに加えてmatch
式を使うことで、第6章で説明したとおり、あらゆるケースが処理できることを、Rustがコンパイル時に保証することになります。
プログラムを書いている時点で、プログラムが実行時に取得し、ベクタに格納し得る全ての型を網羅できない場合には、このenumを使ったテクニックはうまくいかないでしょう。 代わりにトレイトオブジェクトを使用できます。 こちらは第17章で取り上げます。
これまでにベクタの代表的な使い方をいくつか紹介しました。
標準ライブラリでVec<T>
に定義されている多くの有益なメソッドについて、APIドキュメントを必ず確認するようにしてください。
例えば、push
に加えて、pop
というメソッドがあり、これは最後の要素を削除して返します。
それでは次のコレクション型であるString
に移りましょう!