【Rust】RustのVecと仲良くなる
Rustの Vec<T>
(ベクタ)は、動的な配列としてよく使われるコレクション型です。
Rubyなどのインタプリタ言語を普段使ってる方からすると、動的に自動拡張する配列は当たり前かもしれませんが、CやRustのようなコンパイラ言語では、配列は基本的に固定長であり、動的に要素数を変更できるデータ構造は別の仕組みとして提供されます。Rustでは、この役割を果たすのが標準ライブラリの Vec<T>
です。
この記事では、Vecの基本から、所有権や借用、イテレータの活用方法を解説し、Vecと仲良くなることを目的としています。
Vecとは?
The first collection type we’ll look at is Vec<T>, also known as a vector. Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are useful when you have a list of items, such as the lines of text in a file or the prices of items in a shopping cart.
Vec(ベクタ)は、Rustの標準ライブラリで提供される可変長の配列で、同一の型の値のみ格納することができます。
例えば、数値をvecに格納する場合は以下のように書きます
fn main() {
let number_list = vec![1, 2, 3];
println!("{:?}", number_list); // [1, 2, 3]
}
上記の場合、値の型を推論してくれるので型を指定する必要がありまえん。
あまり使用頻度は高くないと思いますが、空のベクタも作成してみます。
fn main() {
let number_list: Vec<i32> = Vec::new();
println!("{:?}", number_list); // []
}
空のベクタを生成する際は、コンパイラ時にどんな値が入るか推測できないため、型注釈を付ける必要があります。
基本操作 (追加・削除・取得)
1.追加
push 末尾に要素を追加
push()
は Vec<T>
の末尾に要素を追加します。
fn main() {
let mut number_list = vec![1, 2, 3];
number_list.push(4);
println!("{:?}", number_list); // [1, 2, 3, 4]
}
insert 任意の位置に挿入
insert()
は指定した位置に要素を挿入できます。
fn main() {
let mut number_list = vec![1, 2, 3];
number_list.insert(0, 0); // 1番目のインデックスに0を挿入
println!("{:?}", number_list); // [0, 1, 2, 3]
}
2.削除
pop 末尾に要素を削除
pop()
は末尾の要素を削除して Option<T>
で返します。
fn main() {
let mut number_list = vec![1, 2, 3, 4];
let last = number_list.pop();
println!("{:?}", number_list); // [1, 2, 3]
println!("{:?}", last); // Some(4)
}
remove 指定した位置の要素を削除
remove()
はインデックスを指定して削除し、その要素を返します。
fn main() {
let mut number_list = vec![1, 2, 3, 4];
let removed = number_list.remove(2);
println!("{:?}", number_list); // [1, 2, 4]
println!("{:?}", removed); // 3
}
3.取得
first / last 最初・最後の要素を取得
最初と最後の要素を Option<T>
で取得します。
fn main() {
let number_list = vec![1, 2, 3, 4];
println!("{:?}", number_list.first()); // Some(1)
println!("{:?}", number_list.last()); // Some(4)
}
添え字記法
[]
は指定した位置の要素を T
または &T
で取得します。
fn main() {
let number_list = vec![1, 2, 3, 4];
println!("{:?}", number_list[1]); // Some(2)
}
get 指定した要素を取得
get()
は指定した位置の要素を Option<T>
で取得します。
fn main() {
let number_list = vec![1, 2, 3, 4];
println!("{:?}", number_list.get(1)); // Some(2)
}
Vecの所有権と借用
Rustの所有権と借用に関する記事はこちら 「【Rust】「Ownership?所有権?何それ・・・」を解決する」
RustのVecはヒープ上にデータを確保し、可変で操作できますが、借用ルールを意識する必要があります。
T と &T の違い
Rustでは T
と &T
は所有権やメモリ管理の観点から異なる性質を持ちます。
T
は値の所有権を持つが、&T
は参照であり、元の値の所有権は保持しないまま借用します。
実験
ベクター内部の各要素がどのメモリアドレスに格納されているかを確認します。
また、添え字記法で借用による取得と、値のコピーによる取得でメモリアドレスの変化を追っていきます。
fn main() {
let number_list = vec![1, 2, 3, 4];
println!("Vec のメモリアドレス: {:p}", &number_list); // Vecの構造体が格納されるアドレスを表示します
for (i, value) in number_list.iter().enumerate() {
// ベクター内部の各要素が格納されているアドレスを表示します
println!("number_list[{}]のアドレス: {:p}", i, value);
}
let index_0_b = &number_list[0]; // 借用
let index_0_c = number_list[0]; // 値のコピー
println!("&number_list[0]のアドレス: {:p}", index_0_b); // 上記で表示したインデックス0のアドレスと同じになります。
println!("number_list[0]のアドレス: {:p}", &index_0_c); // コピーして別メモリに格納されるので、参照するアドレスが変わります
}
出力結果(実行毎にアドレスは変更されます)
Vec のメモリアドレス: 0x16fa06b68
number_list[0]のアドレス: 0x600001c74030 ← インデックス0のアドレス
number_list[1]のアドレス: 0x600001c74034
number_list[2]のアドレス: 0x600001c74038
number_list[3]のアドレス: 0x600001c7403c
&number_list[0]のアドレス: 0x600001c74030 ← インデックス0のアドレス
number_list[0]のアドレス: 0x16fa06ccc ← インデックスの0の値をコピーして別アドレスに格納
結果の考察
-
v[0]
のアドレス(0x600001c74030)
はヒープ領域 に格納されている。 -
index_0_b (&v[0])
は同じアドレスを指しており、借用ではメモリが変わらない。 -
index_0_c (v[0] のコピー)
はスタック上の新しいアドレス(0x16fa06ccc)
に配置されているため、コピーが発生していることが確認できる。
参照後のpush
Rustでは同じスコープ内で 可変参照(&mut)
と 不変参照(&)
を同時に持つこができません。
例えば、ベクターの要素を参照後に新しい要素をpushするコードを書いてみます。
fn main() {
let mut number_list = vec![1, 2, 3, 4]; // 可変なVec
let first = &number_list[0]; // 不変参照を取得
number_list.push(5); // 新しい要素 `5` を追加
println!("{:?}", first);
println!("{:?}", number_list);
}
一見動きそうに見えますが、これだとエラーが発生します。
error[E0502]: cannot borrow `number_list` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &number_list[0]; // 不変参照を取得
| ----------- immutable borrow occurs here
5 |
6 | number_list.push(5); // 新しい要素 `5` を追加
| ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("{:?}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `vec` (bin "vec") due to 1 previous error
理由:
Vec<T> は動的な配列なので、新しい要素を追加するときに内部のメモリ領域が変更される可能性がああり、 let first = &number_list[0];
で借用している値のアドレスが参照できなくなる恐れがあるため、エラーになります。
解決策:
-
push()
の前にfirst
を使い切る - 借用(不変参照)ではなくコピーを使う
- 先に
push()
してメモリを確定させる
先に push() してメモリを確定させる
の実装例
fn main() {
let mut number_list = vec![1, 2, 3, 4]; // 可変なVec
// 先にpushしてメモリ確定させる
number_list.push(5); // 新しい要素 `5` を追加
let first = &number_list[0]; // 不変参照を取得
println!("{:?}", first);
println!("{:?}", number_list);
}
これでエラーが出なくなります。
ベクタを使用する際に、所有権・借用に関するルールを理解することで、上記のような問題もあらかじめ回避できることがわかりました。
どうですか?ベクタの仲良くなりましたか?
最後に、イテレータの活用方法を確認していきます。
イテレータの活用
iter()
を使って、ベクタの要素に対して不変参照のイテレータを取得し、ベクタを安全に反復処理できるようになります。
基本的なループ
fn main() {
let number_list = vec![1, 2, 3, 4];
for n in number_list.iter() {
println!("{}", n);
}
}
このコード、ベクタの各要素を一つずつ出力することができます。
よく使うあれこれ
map()
fn main() {
let number_list = vec![1, 2, 3, 4];
let mapped: Vec<i32> = number_list.iter().map(|x| x * 10).collect();
println!("{:?}", mapped); // [10, 20, 30, 40]
}
sum()
fn main() {
let number_list = vec![1, 2, 3, 4];
let sum: i32 = number_list.iter().sum();
println!("{}", sum); // 10
}
filter()
fn main() {
let number_list = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evens: Vec<i32> = number_list
.iter()
.filter(|&&x| x % 2 == 0)
.copied()
.collect();
println!("{:?}", evens); // [2, 4, 6, 8, 10]
}
まとめ
- Vec<T> はRustでよく使われる動的配列
- push や pop で要素を追加・削除
- get や 添え字記法で指定した要素を取得
- 借用ルールを理解しないとエラーになることがある
- イテレータを活用するとあれこれできる
- 便利!!
Discussion