配列およびVectorコンテナ
Rustの配列は、Cの配列とは全く別物だ。簡単に言うと、固定長配列と可変長配列の両側面を持っているのだ。これらは一般的には、固定長配列と配列スライスとして知られている。
いずれわかることだが、前者の名前はどこか的確でない。だってどちらの配列も固定長(可変長の対義語という意味)だからね。可変長配列カテゴリとしてRustでは、Vecというコレクションクラスが用意されている。
固定長配列
固定長配列の配列長は、コンパイル時に決定され、一つの型と考えられる。例: [i32; 4]は配列長4のi32型の配列。
配列のリテラル表記やアクセス方法は、Cと変わらない。
let a: [i32; 4] = [1, 2, 3, 4]; // もちろん、型注記は必須ではない println!("二番目の要素は{}", a[1]);配列のインデックスがC同様、0起点のことに気づいたかな。
ところが、CやC++と異なり、インデックスは境界値チェックが行われる。事実、配列へのアクセスはすべて境界値チェックが行われる。これもRustがより安全な言語と言える根拠の一つだ。
ここでa[4]と書こうとすると、ランタイムエラーが発生する。残念ながら、Rustのコンパイラはコンパイルエラーを出してくれるほど賢明ではないのだ。上記の例みたいにエラーになるのが一目瞭然であってもね。
あなたが危ない橋を渡りたがりだったり、プログラムのパフォーマンスを極限まで追求しなきゃいけない場合、境界値チェックを行わない方法もある。配列に対して、get_uncheckedメソッドを呼び出せばいい。境界値非チェックは、unsafeブロックで行う必要がある。こんなことをする機会は極々少ないはずだ。
他のデータ構造同様、Rustにおいて、配列は規定で不変になり、可変性は伝播する。また、インデックスアクセスでも可変化できる。
let mut a = [1, 2, 3, 4]; a[3] = 5; println!("{:?}", a);さらに、参照を取ることで配列を無所有権参照することもできる。
fn foo(a: &[i32; 4]) { println!("先頭: {}; 最後: {}", a[0], a[3]); } fn main() { foo(&[1, 2, 3, 4]); }無所有権参照された配列でも、インデックスアクセスできることに注目してほしい。
ここら辺で、C++プログラマーにとってRustの配列の気になりやすい側面について語っておこう。メモリ構造だ。
Rustの配列は、値型である。つまり、配列は他の値同様、スタック上に確保され、配列オブジェクトは(Cで見られるような)値へのポインタではなく、データ列で表現される。
ゆえに前述の例で、let a = [1_i32, 2, 3, 4];は、スタック上に16バイトのメモリ領域を確保し、let b = a;と書けば、さらにこの16バイトの領域をコピーする。Cのような配列が必要なら、明示的に配列へのポインタを作成する必要がある。そうすれば、先頭要素へのポインタが得られる。
最後に、Rustの配列はtraitを実装できる。ゆえに、メソッドを持っている。
なので、配列長を得るには、a.len()を呼び出せばいい。
配列スライス
Rustにおける配列スライスは、ただ単に配列長がコンパイル時に決定していないだけの配列である。型の表記法も、固定長配列と同じであるが、長さを明示する必要はない。具体例: [i32]は32ビット整数の配列スライス(配列長は動的に決まる)だ。
ただ、配列スライスには落とし穴がある。Rustにおいて、コンパイラは全オブジェクトのサイズを把握しておく必要があり、配列スライスのサイズは把握できないから、配列スライスを値で保持することはできないのだ。fn foo(x: [i32])などと書こうものなら、たちまちコンパイラがエラーを吐くだろう。
そこで、配列スライスは常にポインタで保持しなければならない(このルールには、オレオレスマポを実装するという非常に高度な技術を要する例外があるが、今は無視しても構わない)。要約すると、(配列スライスを無所有権参照するなら)fn foo(x: &[i32])、(配列スライスに対して可変生ポインタを得るなら)fn foo(x: *mut [i32])などと書かねばならない。
配列スライスを生成する最も単純な方法は、簡略化を使用することだ。
C++と比較して、Rustの暗黙的簡略化は、はるかに少ない。そのうちの一つが、固定長配列から配列スライスへの簡略化である。配列スライスはポインタでなければならないから、これは実質的にポインタ間の簡略化になる。
例えば、&[i32; 4]から&[i32]への簡略化は以下のようにして行える。
let a: &[i32] = &[1, 2, 3, 4];ここで、右辺はスタック上に確保された配列長4の固定長配列である。そこから参照(型は&[i32; 4])を取り、この参照が&[i32]型に簡略化され、let文によりaという名前を与えられている。
配列スライスもまた、C同様に([...]で)アクセスし、境界値チェックが行われる。len()メソッドを呼び出せば、手動で配列長を確認することもできる。したがって、いずれかの時点で配列長が決定するのは明白だ。
実際、Rustの配列はどんな種類でも、配列長が決まっており、これが境界値チェックの肝であるため、メモリ安全性に貢献する要素たるのだ。
サイズは(固定長配列の静的決定と対称的に)動的に定まるため、配列スライスは動的サイズ付けタイプ(英語略称: DST - Dynamically Sized Types. 他にも動的にサイズが決定する型があるので、別の機会に解説する)と呼ばれる。
配列スライスオブジェクトは、ただのデータ列でしかないため、オブジェクト内にサイズは格納できない。代わりに、サイズはポインタの一部になる(配列スライスは、絶対にポインタでなければならないことを覚えているだろうか)。
(他の動的サイズ付けタイプへのポインタ同様)配列スライスへのポインタは、非正規化ポインタ(訳注: 造語; fat pointer - regular pointerが1ワード長に対して、それよりもサイズが大きいので)になる。非正規化ポインタとは、1ワード長ではなく、2ワード長で、データを指すポインタと追加のデータを持つものである。配列スライスの場合、追加データは、スライスの長さになる。
以上より、上記の例でポインタaは(64ビットシステム上では)128ビット長になり、前半部分にデータ列の先頭となる1へのメモリアドレスが、後半には長さの4が格納される。
Rustのプログラマー的に普段は、これら非正規化ポインタも通常のポインタと同等に扱って構わないが、少しでもかじっておくのはいいことだ(具体的には、キャストでできることなどに影響してくる)。
スライス記法と範囲オブジェクト
配列スライスは、配列に対する(無所有権参照の)窓と捉えることができる。ここまで、配列全体のスライスしか見てこなかったが、それ以外にも配列の一部をスライス化することもできる。これには、インデックス表記に似た特殊な書き方があり、整数を一つ取る代わりに範囲オブジェクトを与えることで作用させる。具体例: a[0..4]は配列aの先頭4要素をスライス化する。
範囲オブジェクトは、上限は含まず、下限は含むことに注意してほしい。
let a: [i32; 4] = [1, 2, 3, 4]; let b: &[i32] = &a; // 配列全体をスライス化 let c = &a[0..4]; // bの別表記。型も&[i32] let c = &a[1..3]; // 中間2要素。型は&[i32] let c = &a[1..]; // 末尾3要素 let c = &a[..3]; // 先頭3要素 let c = &a[..]; // またまたbの別表記 let c = &b[1..3]; // 配列スライスもスライス化できる最後の例において、スライスに対しても、無所有権参照をする必要があることに注目してほしい。スライス記法は、スライスオブジェクト(型は[i32])を生成する。なので、無所有権参照されたスライスオブジェクトをスライス化していても、これを無所有権参照(そうすれば&[i32]型のオブジェクトが得られる)しなければならない。
範囲記法は、スライス記法以外でも使用できる。a..bという書き方は、aからb-1までを列挙するイテレータを生成するのだ。なので、これを通常通り他のイテレータと混ぜたり、forループで使用したりできる。
// 1から10までの整数を出力 for i in 1..11 { println!("{}", i); }
ベクターコンテナ
ベクターオブジェクトは、ヒープ領域に確保される有所有権参照である。従って、(Box<_>のように)ムーブ機構に順応している。固定長配列を値、配列スライスを無所有権参照とみなすと、RustのベクターコンテナはBox<_>ポインタに相当すると考えられる。
こうすれば、Vec<_>型をBox<_>型同様に、値というよりも一種のスマポのように考えやすくなる。配列スライスと同じく、長さはポインタに格納され、この場合、そのポインタとはベクターオブジェクトになる。
i32型のベクターはVec<i32>型になる。ベクターにリテラル表記はないが、vec!マクロを使えば同じ効果が得られる。また、Vec::new()メソッドで空のベクターも生成できる。
let v = vet![1, 2, 3, 4]; // 長さ4のVec上の例の2行目では、コンパイラがベクターの中身の型を把握できるように型注記が必須である。このベクターを使うつもりがあるなら、(翻訳者注: 使用箇所から型が推論できるので)この型注記は必要なくなるだろう。オブジェクト let v: Vec<i32> = Vec::new(); // i32型の空ベクター
配列や配列スライス同様、インデックス記法でベクターから値を取り出す(例: v[2])ことができ、これも境界値チェックが行われる。また、スライス記法でベクターをスライス化する(例: &v[1..3])こともできる。
ベクターにしかない機能として、サイズが動的に変化することが挙げられ、必要に応じて、伸びもするし、縮みもする。例えば、v.push(5)と書けば、ベクターの末尾に要素5を追加する(この時、変数vは可変でなければならない)。
なお、ベクターを肥大化させるとメモリの再確保が必要になることがあり、巨大なベクター相手だと、大量のコピーが発生することになる。これを防ぐには、with_capacityメソッドで予め大きな領域を割り当てておけばいい。詳しくはVec docs(英語版)を参照されたし。
Index trait
注意喚起: この節は、まだまともに解説できてない要素がたくさんある。チュートリアルを辿っているのなら、飛ばしても構わない。どう考えても、話題が高度すぎる。
配列やベクターと全く同じインデックス記法をHashMapなどのその他のコレクションクラスにも使用することができる。その上、自作のコレクションクラスにも応用できる。
インデックス記法(とスライス記法)の使用を宣言するには、Index traitを実装する。これは、Rustにおいて、組み込み型のみならず、ユーザ定義タイプでも略記法を使用できるようにする良い例になる(Derefでスマポの被参照を行え、その他Add traitなども含め、似たような挙動をする)。
Index traitは以下のような定義をしている。
pub trait Index<idx: ?Sized> { type Output: ?Sized; fn index(&self, index: Idx) -> &Self::Output; }Idxは添え字の型を表し、インデックス記法なら、だいたいusizeになり、スライス記法ならstd::ops::Range系のいずれかになる。
Outputは、インデックス記法で返される型を表し、コレクションごとに異なる。スライス記法の時は、単一要素の型というよりも、スライスオブジェクトになる。
indexメソッドは、実際にコレクションから要素を取り出す役割を担っている。ちなみに、コレクションは参照で渡され、このメソッドは(翻訳者注: 要素と)同じライフタイムを持つ要素への参照を返す。
Vecクラスの実装を覗いて、どんな実装になるかを見てみよう。
impl<t> Index<usize> for Vec<t> { type Output = T; fn index(&self, index: usize) -> &T { &(**self)[index] } }前述した通り、インデックス記法はusize型を使っている。Vec<T>型オブジェクトに対して、インデックス記法だとT型の単一要素、つまりOutput型の値が得られる。
indexメソッドの実装は少し奇妙だ。(**self)でベクター全体をスライス化し、インデックス記法で要素を取り出し、最後にこの要素への参照を取っている。
自作のコレクションクラスに対しても、同様のIndex実装を行って、インデックス記法やスライス記法を使うことができる。
初期化子記法
他の種々のデータ同様、Rustにおいて、配列とベクターは適切に初期化されなければならない。しばしば、0埋めされた配列が必要になるが、これをリテラル表記で書くのは苦痛でしかない。そのため、Rustには配列を特定値で初期化する糖衣構文が用意されており、[value; len]と書く。なので、長さ100の0埋め配列を作成するには、[0; 100]と書けば良い。
ベクターでも、vec![42; 100]と書けば、長さ100の42という値で埋められたベクターオブジェクトが得られる。
また、初期値は整数に限定されず、どんな式でもいい。
配列初期化式なら、長さは整数定数式でなければならず、vec!マクロなら、usize型の式にする。
原文: https://github.com/nrc/r4cppp/blob/master/arrays.md
0 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。