データ型
Rust におけるすべての値は、ある データ型 を持っています。データ型は、どの種類の データが指定されているのかを Rust に伝え、そのデータをどう扱うべきかを Rust が理解 できるようにします。ここでは、データ型の 2 つのサブセットであるスカラー型と複合型を 見ていきます。
Rust は 静的型付け 言語であることを覚えておいてください。つまり、コンパイル時に
すべての変数の型がわかっていなければなりません。コンパイラは通常、値とその使い方に
基づいて、私たちが使いたい型を推論できます。第 2 章の
「予想と秘密の数を比較する」 節で
parse を使って String を数値型に変換したときのように、取り得る型が複数ある場合
には、次のように型注釈を追加しなければなりません。
#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}
前のコードに示した : u32 という型注釈を追加しないと、Rust は次のエラーを表示しま
す。これは、どの型を使いたいのかをコンパイラが判断するために、私たちからさらに多く
の情報を必要としていることを意味します。
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
他のデータ型では、異なる型注釈を目にすることになります。
スカラー型
スカラー 型は単一の値を表します。Rust には 4 つの主要なスカラー型があります。 整数、浮動小数点数、ブール値、文字です。これらは他のプログラミング言語でも見覚えが あるかもしれません。Rust でどのように機能するのかを見ていきましょう。
整数型
整数 とは、小数部分を持たない数です。第 2 章では、整数型の 1 つである u32 型を
使いました。この型宣言は、関連付けられる値が、32 ビットの領域を使用する符号なし整数
であるべきことを示しています(符号付き整数型は u ではなく i で始まります)。
表 3-1 は Rust に組み込まれている整数型を示しています。整数値の型を宣言するには、
これらのバリアントのいずれでも使えます。
表 3-1: Rust における整数型
| 長さ | 符号付き | 符号なし |
|---|---|---|
| 8 ビット | i8 | u8 |
| 16 ビット | i16 | u16 |
| 32 ビット | i32 | u32 |
| 64 ビット | i64 | u64 |
| 128 ビット | i128 | u128 |
| アーキテクチャ依存 | isize | usize |
各バリアントは符号付きまたは符号なしのいずれかで、明示的なサイズを持ちます。 符号付き と 符号なし は、その数が負になり得るかどうかを指します。言い換えると、 その数に符号が必要か(符号付き)、あるいは常に正であり、そのため符号なしで表現 できるか(符号なし)ということです。これは紙に数を書くときと似ています。符号が 重要なときは数はプラス記号またはマイナス記号とともに書かれますが、その数が正だと 考えて差し支えないときは符号なしで書かれます。符号付き数は 2 の補数 表現で格納されます。
各符号付きバリアントは、−(2n − 1) から 2n − 1 − 1 まで
(両端を含む)の数を格納できます。ここで n はそのバリアントが使用するビット数
です。したがって、i8 は −(27) から 27 − 1、つまり
−128 から 127 までの数を格納できます。符号なしバリアントは 0 から
2n − 1 までの数を格納できるので、u8 は 0 から
28 − 1、つまり 0 から 255 までの数を格納できます。
さらに、isize と usize 型は、プログラムが実行されるコンピューターの
アーキテクチャに依存します。64 ビットアーキテクチャでは 64 ビット、
32 ビットアーキテクチャでは 32 ビットです。
整数リテラルは、表 3-2 に示す任意の形式で記述できます。複数の数値型になり得る
数値リテラルでは、57u8 のように型サフィックスを付けて型を指定できることに注意
してください。数値リテラルでは _ を視覚的な区切りとして使って読みやすくする
こともできます。たとえば 1_000 は 1000 を指定した場合と同じ値になります。
表 3-2: Rust における整数リテラル
| 数値リテラル | 例 |
|---|---|
| 10 進数 | 98_222 |
| 16 進数 | 0xff |
| 8 進数 | 0o77 |
| 2 進数 | 0b1111_0000 |
バイト(u8 のみ) | b'A' |
では、どの整数型を使えばよいのでしょうか。よくわからない場合は、Rust のデフォルト
から始めるのが一般的にはよいでしょう。整数型のデフォルトは i32 です。isize
または usize を使う主な場面は、何らかのコレクションにインデックスを付けるとき
です。
整数オーバーフロー
0 から 255 までの値を保持できる
u8型の変数があるとします。その変数を 256 の ような範囲外の値に変更しようとすると、整数オーバーフロー が発生し、2 つの 振る舞いのいずれかになります。デバッグモードでコンパイルすると、Rust は整数 オーバーフローを検査するチェックを組み込み、この挙動が発生した場合に実行時に プログラムを パニック させます。Rust では、プログラムがエラーで終了することを パニックする と呼びます。パニックについては、第 9 章の 「panic!による回復不能なエラー」 節で さらに詳しく説明します。
--releaseフラグ付きのリリースモードでコンパイルすると、Rust はパニックを 引き起こす整数オーバーフローのチェックを 含めません。その代わり、オーバー フローが起こると、Rust は 2 の補数によるラップアラウンド を行います。要するに、 その型が保持できる最大値より大きい値は、その型が保持できる最小値へと「折り返され」 ます。u8の場合、256 は 0 になり、257 は 1 になり、以下同様です。プログラムは パニックしませんが、その変数にはおそらく期待していたものとは異なる値が入ります。 整数オーバーフローのラップアラウンド動作に依存することは、エラーと見なされます。オーバーフローの可能性を明示的に扱うには、標準ライブラリがプリミティブ数値型に 提供している次のメソッド群を利用できます。
wrapping_addなどのwrapping_*メソッドで、すべてのモードでラップする。checked_*メソッドで、オーバーフローがあればNone値を返す。overflowing_*メソッドで、値と、オーバーフローがあったかどうかを示す ブール値を返す。saturating_*メソッドで、値の最小値または最大値で飽和させる。
浮動小数点型
Rust には 浮動小数点数 のためのプリミティブ型も 2 つあります。これは小数点を
持つ数です。Rust の浮動小数点型は f32 と f64 で、それぞれ 32 ビットと
64 ビットのサイズです。デフォルトの型は f64 です。これは、現代の CPU では
f32 とほぼ同じ速度でありながら、より高い精度を持てるためです。すべての
浮動小数点型は符号付きです。
次は、浮動小数点数がどのように動作するかを示す例です。
ファイル名: src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮動小数点数は IEEE-754 標準に従って表現されます。
数値演算
Rust は、すべての数値型に対して、期待どおりの基本的な数学演算をサポートしています。すなわち、加算、減算、乗算、除算、剰余です。整数の除算は、ゼロ方向に切り捨てて最も近い整数になります。次のコードは、各数値演算を let 文でどのように使うかを示しています。
ファイル名: src/main.rs
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
これらの文に含まれる各式は数学演算子を使用し、単一の値に評価され、その後変数に束縛されます。付録 B には、Rust が提供するすべての演算子の一覧があります。
真偽値型
他の多くのプログラミング言語と同様に、Rust の真偽値型は true と false の 2 つの値を取りえます。真偽値のサイズは 1 バイトです。Rust における真偽値型は bool で表されます。たとえば次のとおりです。
ファイル名: src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
真偽値を使う主な方法は、if 式のような条件分岐を通じてです。Rust における if 式の動作については、「制御フロー」 の節で扱います。
文字型
Rust の char 型は、この言語における最も基本的なアルファベット型です。char 値の宣言例をいくつか示します。
ファイル名: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
char リテラルはダブルクォートを使う文字列リテラルとは異なり、シングルクォートで指定する点に注意してください。Rust の char 型は 4 バイトの大きさを持ち、Unicode スカラー値を表します。つまり、単なる ASCII よりもはるかに多くのものを表現できます。アクセント付き文字、中国語・日本語・韓国語の文字、絵文字、ゼロ幅スペースはいずれも Rust では有効な char 値です。Unicode スカラー値の範囲は U+0000 から U+D7FF および U+E000 から U+10FFFF までです。ただし、「文字」は Unicode において実際には厳密な概念ではないため、「文字」とは何かについての人間の直感は、Rust における char と一致しない場合があります。この話題については、第 8 章の 「文字列による UTF-8 エンコード済みテキストの格納」 で詳しく説明します。
複合型
複合型 は、複数の値を 1 つの型にまとめることができます。Rust には 2 つの基本的な複合型があります。タプルと配列です。
タプル型
タプル は、さまざまな型の複数の値を 1 つの複合型にまとめる一般的な方法です。タプルの長さは固定です。いったん宣言すると、大きくしたり小さくしたりはできません。
タプルは、丸括弧の中にカンマ区切りの値のリストを書いて作成します。タプル内の各位置には型があり、タプル内の各値の型は同じである必要はありません。この例では、任意の型注釈を追加しています。
ファイル名: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
変数 tup は、タプル全体に束縛されます。これは、タプルが 1 つの複合要素と見なされるためです。タプルから個々の値を取り出すには、次のようにパターンマッチングを使ってタプル値を分解できます。
ファイル名: src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
このプログラムはまずタプルを作成し、それを変数 tup に束縛します。次に、let とともにパターンを使って tup を取り出し、3 つの別々の変数 x、y、z にします。これは、1 つのタプルを 3 つの部分に分解するため、分配束縛 と呼ばれます。最後に、プログラムは y の値、すなわち 6.4 を表示します。
また、アクセスしたい値のインデックスをピリオド (.) に続けて指定することで、タプル要素に直接アクセスすることもできます。たとえば次のとおりです。
ファイル名: src/main.rs
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
このプログラムはタプル x を作成し、その後それぞれのインデックスを使って各要素にアクセスします。ほとんどのプログラミング言語と同様に、タプルの最初のインデックスは 0 です。
値をまったく持たないタプルには、特別な名前として unit があります。この値とそれに対応する型はどちらも () と書き、空の値または空の戻り値型を表します。式が他の値を返さない場合、暗黙的に unit 値を返します。
配列型
複数の値のコレクションを持つもう 1 つの方法は、配列 を使うことです。タプルとは異なり、配列のすべての要素は同じ型でなければなりません。また、一部の他言語の配列とは異なり、Rust の配列は長さが固定です。
配列の値は、角括弧の中にカンマ区切りのリストとして書きます。
ファイル名: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
配列は、ここまでに見てきた他の型と同じように、データをヒープではなくスタックに確保したい場合(スタックとヒープについては 第 4 章 でさらに詳しく説明します)、あるいは常に固定数の要素を持つことを保証したい場合に便利です。ただし、配列はベクタ型ほど柔軟ではありません。ベクタは標準ライブラリが提供する類似のコレクション型で、その内容がヒープ上に置かれるため、サイズを大きくしたり小さくしたりできます。配列とベクタのどちらを使うべきか迷うなら、たいていの場合はベクタを使うべきです。第 8 章 ではベクタについてさらに詳しく説明します。
ただし、要素数が変わる必要がないことが分かっている場合には、配列のほうが有用です。たとえば、プログラムで月の名前を使う場合、常に 12 個の要素を含むことが分かっているため、ベクタではなくおそらく配列を使うでしょう。
#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}
配列の型は、各要素の型、セミコロン、そして配列内の要素数を角括弧で囲んで、次のように書きます。
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
ここで、i32 は各要素の型です。セミコロンの後の数字 5 は、その配列が 5 個の要素を含むことを示しています。
また、初期値、その後にセミコロン、そして角括弧内に配列の長さを指定することで、各要素に同じ値を持つ配列を初期化することもできます。次のようになります。
#![allow(unused)]
fn main() {
let a = [3; 5];
}
a という名前の配列には 5 個の要素が含まれ、それらは最初すべて値 3 に設定されます。これは let a = [3, 3, 3, 3, 3]; と書くのと同じですが、より簡潔です。
配列要素へのアクセス
配列は、既知の固定サイズを持つ単一のメモリ領域であり、スタックに割り当てることができます。配列の要素には、次のようにインデックスを使ってアクセスできます。
<span class="filename">ファイル名: src/main.rs</span>
```rust
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
この例では、first という名前の変数は値 1 を受け取ります。これは、その値が配列のインデックス [0] にあるためです。second という名前の変数は、配列のインデックス [1] から値 2 を受け取ります。
無効な配列要素へのアクセス
配列の末尾を越えた要素にアクセスしようとすると何が起こるのかを見てみましょう。第2章の数当てゲームと同様に、ユーザーから配列のインデックスを受け取るために、次のコードを実行するとします。
ファイル名: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
このコードは問題なくコンパイルされます。このコードを cargo run で実行して 0、1、2、3、または 4 を入力すると、プログラムは配列内のそのインデックスに対応する値を出力します。代わりに、配列の末尾を越える数、たとえば 10 を入力すると、次のような出力が表示されます。
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
このプログラムは、インデックス操作で無効な値を使った時点で実行時エラーになりました。プログラムはエラーメッセージを表示して終了し、最後の println! 文は実行されませんでした。インデックスを使って要素にアクセスしようとすると、Rust は指定したインデックスが配列の長さより小さいことを確認します。インデックスが長さ以上であれば、Rust は panic します。このチェックは実行時に行われる必要があります。特にこの場合、ユーザーが後でコードを実行するときにどんな値を入力するかを、コンパイラが知ることは不可能だからです。
これは、Rust のメモリ安全性の原則が実際に機能している一例です。多くの低水準言語では、この種のチェックは行われず、誤ったインデックスを与えると無効なメモリにアクセスできてしまいます。Rust は、メモリアクセスを許可して処理を継続するのではなく、直ちに終了することで、この種のエラーから保護してくれます。第9章では、Rust のエラーハンドリングについてさらに詳しく扱い、panic せず、また無効なメモリアクセスも許さない、読みやすく安全なコードをどのように書けるかを説明します。