配列とベクター
Rust の配列は C の配列とはかなり異なります。まず、静的サイズと動的サイズの種類があります。これらはより一般的には固定長配列とスライスとして知られています。これから見るように、前者はやや不適切な名前です。なぜなら、どちらの種類の配列も(拡張可能ではなく)固定された長さを持つからです。拡張可能な「配列」として、Rust は Vec コレクションを提供しています。
固定長配列
固定長配列の長さは静的に分かっており、その型に含まれます。たとえば、[i32; 4] は長さ 4 の i32 の配列の型です。
配列リテラルと配列アクセスの構文は C と同じです。
#![allow(unused)] fn main() { let a: [i32; 4] = [1, 2, 3, 4]; // 通常どおり、型注釈は任意です。 println!("The second element is {}", a[1]); }
配列のインデックスが C と同じく 0 始まりであることに気づくでしょう。
しかし、C/C++1 とは異なり、配列のインデックス指定は境界チェックされます。実際、配列へのすべてのアクセスは境界チェックされます。これは Rust がより安全な言語であるもう 1 つの理由です。
a[4] を実行しようとすると、実行時パニックが発生します。残念ながら、この例のように明らかな場合であっても、Rust コンパイラはコンパイル時エラーを出せるほど賢くはありません。
危険を承知で使いたい場合や、プログラムから最後の一滴まで性能を引き出す必要がある場合でも、配列への未チェックアクセスを行うことはできます。これを行うには、配列の get_unchecked メソッドを使用します。未チェックの配列アクセスは unsafe ブロック内になければなりません。これが必要になるのは、ごくまれな状況に限られるはずです。
Rust の他のデータ構造と同じように、配列はデフォルトでイミュータブルであり、ミュータビリティは継承されます。変更もインデックス指定構文を通じて行います。
#![allow(unused)] fn main() { let mut a = [1, 2, 3, 4]; a[3] = 5; println!("{:?}", a); }
また他のデータと同じように、配列への参照を取ることで配列を借用できます。
fn foo(a: &[i32; 4]) { println!("First: {}; last: {}", 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 と C++ の配列における最後の違いは、Rust の配列はトレイトを実装でき、そのためメソッドを持てることです。たとえば、配列の長さを調べるには a.len() を使います。
スライス
Rust におけるスライスとは、単に長さがコンパイル時に分からない配列です。型の構文は固定長配列と同じですが、長さがありません。たとえば、[i32] は 32 ビット整数のスライス(静的に分かる長さを持たないもの)です。
スライスには注意点があります。Rust ではコンパイラがすべてのオブジェクトのサイズを知っていなければならず、スライスのサイズは知ることができないため、スライス型の値を持つことは決してできません。たとえば fn foo(x: [i32]) と書こうとすると、コンパイラはエラーを出します。
そのため、常にスライスへのポインターを持たなければなりません(この規則には、自作のスマートポインターを実装できるようにするための非常に技術的な例外がいくつかありますが、今のところは安全に無視できます)。fn foo(x: &[i32])(スライスへの借用参照)や fn foo(x: *mut [i32])(スライスへのミュータブルな生ポインター)などと書く必要があります。
スライスを作成する最も簡単な方法は型強制です。Rust には C++ よりもはるかに少ない暗黙の型強制しかありません。その 1 つが固定長配列からスライスへの型強制です。スライスはポインター値でなければならないため、これは実質的にはポインター間の型強制です。たとえば、&[i32; 4] を &[i32] に型強制できます。
#![allow(unused)] fn main() { let a: &[i32] = &[1, 2, 3, 4]; }
ここで右辺は、スタック上に割り当てられた長さ 4 の固定長配列です。次に、その参照を取ります(型は &[i32; 4])。その参照は &[i32] 型へ型強制され、let 文によって a という名前が与えられます。
ここでも、アクセスは C と同じく([...] を使って)行い、アクセスは境界チェックされます。len() を使って自分で長さを確認することもできます。したがって、配列の長さはどこかで分かっていることは明らかです。実際、Rust のあらゆる種類の配列は既知の長さを持っています。これは境界チェックに不可欠であり、境界チェックはメモリ安全性の不可欠な部分だからです。サイズは(固定長配列の場合のように静的にではなく)動的に分かっており、スライス型は動的サイズ型(DST。他にも動的サイズ型の種類があり、それらは別の箇所で扱います)であると言います。
スライスは単なる値の列なので、そのサイズをスライスの一部として格納することはできません。代わりに、ポインターの一部として格納されます(スライスは常にポインター型として存在しなければならないことを思い出してください)。スライスへのポインター(DST へのすべてのポインターと同様)はファットポインターです。つまり、1 ワードではなく 2 ワード幅で、データへのポインターに加えてペイロードを含みます。スライスの場合、ペイロードはスライスの長さです。
したがって上の例では、ポインター a は(64 ビットシステム上で)128 ビット幅になります。最初の 64 ビットには列 [1, 2, 3, 4] の中の 1 のアドレスが格納され、2 つ目の 64 ビットには 4 が含まれます。通常、Rust プログラマーとしては、これらのファットポインターを通常のポインターとして扱えば問題ありません。しかし、それについて知っておくのはよいことです(たとえば、キャストでできることに影響する場合があります)。
スライス記法と範囲
スライスは、配列の(借用された)ビューと考えることができます。これまでは配列全体のスライスだけを見てきましたが、配列の一部のスライスを取ることもできます。そのための特別な記法があり、インデックス指定構文に似ていますが、単一の整数ではなく範囲を取ります。たとえば a[0..4] は、a の最初の 4 要素のスライスを取ります。範囲は上端を含まず、下端を含むことに注意してください。例:
#![allow(unused)] fn main() { let a: [i32; 4] = [1, 2, 3, 4]; let b: &[i32] = &a; // 配列全体のスライス。 let c = &a[0..4]; // 配列全体の別のスライスで、これも型は &[i32]。 let c = &a[1..3]; // 中央の 2 要素、&[i32]。 let c = &a[1..]; // 最後の 3 要素。 let c = &a[..3]; // 最初の 3 要素。 let c = &a[..]; // 再び、配列全体。 let c = &b[1..3]; // スライスをさらにスライスすることもできます。 }
最後の例では、スライスした結果をなお借用する必要があることに注意してください。スライス構文は借用されていないスライス(型: [i32])を生成し、それをさらに借用する必要があります(&[i32] にするため)。これは、借用されたスライスをスライスしている場合でも同じです。
範囲構文はスライス構文の外でも使用できます。a..b は、a から b-1 まで進むイテレーターを生成します。これは通常どおり他のイテレーターと組み合わせることも、for ループで使用することもできます。
#![allow(unused)] fn main() { // 1 から 10 までのすべての数値を表示します。 for i in 1..11 { println!("{}", i); } }
Vec
ベクターはヒープに割り当てられ、所有参照です。そのため(Box<_> と同様に)、ムーブセマンティクスを持ちます。固定長配列は値に、スライスは借用参照になぞらえて考えることができます。同様に、Rust のベクターは Box<_> ポインターに似ています。
Vec<_> は値そのものとしてではなく、Box<_> と同じようなスマートポインターの一種として考えると理解しやすくなります。スライスと同様に、長さは「ポインター」に格納されます。この場合の「ポインター」は Vec の値です。
i32 のベクターは Vec<i32> 型です。ベクターリテラルはありませんが、vec! マクロを使うことで同じ効果を得られます。また、Vec::new() を使って空のベクターを作成することもできます。
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4]; // 長さ4のVec<i32>。 let v: Vec<i32> = Vec::new(); // i32の空のベクター。 }
上の2つ目のケースでは、ベクターが何のベクターなのかをコンパイラーが知ることができるように、型注釈が必要です。そのベクターを実際に使う場合には、型注釈はおそらく不要でしょう。
配列やスライスと同じように、インデックス表記を使ってベクターから値を取得できます(例: v[2])。ここでも境界チェックが行われます。また、スライス表記を使ってベクターのスライスを取ることもできます(例: &v[1..3])。
ベクターの追加機能は、そのサイズを変更できることです。必要に応じて長くしたり短くしたりできます。たとえば、v.push(5) は要素 5 をベクターの末尾に追加します(これには v がミュータブルである必要があります)。ベクターを大きくすると再割り当てが発生する可能性があり、大きなベクターでは大量のコピーを意味する場合があることに注意してください。これを防ぐために、with_capacity を使ってベクター内の領域を事前に割り当てることができます。詳細については Vec docs を参照してください。
Index トレイト
読者への注意: このセクションには、まだ十分に扱っていない内容がたくさんあります。チュートリアルに沿って読んでいる場合、このセクションは飛ばしてもかまいません。いずれにせよ、やや高度なトピックです。
配列やベクターに使われるのと同じインデックス構文は、HashMap などの他のコレクションにも使われます。また、自分自身のコレクションにも使うことができます。Index トレイトを実装することで、インデックス(およびスライス)構文の使用を選択できます。これは、Rust が組み込み型だけでなくユーザー型にも便利な構文を提供する仕組みの良い例です(スマートポインターの参照外しに使う Deref や、Add およびその他さまざまなトレイトも、同様の方法で機能します)。
Index トレイトは次のようなものです。
#![allow(unused)] fn main() { pub trait Index<Idx: ?Sized> { type Output: ?Sized; fn index(&self, index: Idx) -> &Self::Output; } }
Idx はインデックスに使われる型です。インデックスのほとんどの用途では、これは usize です。スライスでは、これは std::ops::Range 型のいずれかです。Output はインデックスによって返される型で、これはコレクションごとに異なります。スライスの場合は、単一要素の型ではなくスライスになります。index は、コレクションから要素を取り出す処理を行うメソッドです。コレクションは参照で受け取られ、このメソッドは同じライフタイムを持つ要素への参照を返すことに注意してください。
実装がどのようなものかを確認するために、Vec の実装を見てみましょう。
#![allow(unused)] fn main() { 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) は vec 全体をスライスとして見るビューを取得し、その後スライスに対するインデックスを使って要素を取得し、最後にその参照を取っています。
自分自身のコレクションがある場合、同様の方法で Index を実装することで、そのコレクションに対してインデックス構文やスライス構文を使えるようにできます。
初期化子構文
Rust のすべてのデータと同様に、配列とベクターは適切に初期化されていなければなりません。多くの場合、最初はゼロで埋められた配列が欲しいだけであり、配列リテラル構文を使うのは面倒です。そこで Rust は、指定した値で埋められた配列を初期化するためのちょっとした構文糖を用意しています: [value; len]。たとえば、長さ100でゼロ埋めされた配列を作成するには、[0; 100] を使います。
ベクターについても同様に、vec![42; 100] は100個の要素を持ち、それぞれの値が42であるベクターを生成します。
初期値は整数に限定されず、任意の式にできます。配列初期化子では、長さは整数定数式でなければなりません。vec! では、usize 型の任意の式にできます。
-
C++11 には
std::array<T, N>があり、at()メソッドが使われるときに境界チェックを提供します。 ↩