データ型
Rustにおける値は全て、何らかのデータ型になり、コンパイラがどんなデータが指定されているか知れるので、 そのデータの取り扱い方も把握できるというわけです。2種のデータ型のサブセットを見ましょう: スカラー型と複合型です。
Rustは静的型付き言語であることを弁えておいてください。つまり、
コンパイル時に全ての変数の型が判明している必要があるということです。コンパイラは通常、値と使用方法に基づいて、
使用したい型を推論してくれます。複数の型が推論される可能性がある場合、例えば、
第2章の「予想と秘密の数字を比較する」節でparseメソッドを使ってString型を数値型に変換した時のように、
複数の型が可能な場合には、型注釈をつけなければいけません。以下のようにですね:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); // 数字ではありません! }
上のコード中に示す: u32型注釈を付けなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、
どの型を使用したいのかを知るのに、コンパイラがプログラマからもっと情報を得る必要があることを意味します:
$ 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 == _`
(`<_ as FromStr>::Err == _`を満たすことができません)
help: consider giving `guess` an explicit type
| (`guess`に型を与えることを検討してください)
|
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章で一つの整数型を使用しましたね。u32型です。
この型定義は、紐付けられる値が、符号なし整数(符号付き整数はuではなく、iで始まります)になり、
これは、32ビット分のサイズを取ります。表3-1は、Rustの組み込み整数型を表示しています。
これらのバリアントを使用して、整数値の型を宣言することができます。
表3-1: Rustの整数型
| 大きさ | 符号付き | 符号なし |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | 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型は、表では「arch」と表記していますが、プログラムが動作しているコンピュータのアーキテクチャに依存します:
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を使う主な状況は、何らかのコレクションにアクセスすることです。
整数オーバーフロー
u8型の変数があるとしましょう。u8は0から255までの間の値を取ることができます。 この変数を範囲外の値、例えば256に変更しようとすると、整数オーバーフロー (integer overflow) が発生し、次の2つのうちのどちらかの挙動になります。 デバッグモードでコンパイルしているときは、もしこの挙動が発生したときは実行時にプログラムをパニック (panic) させるような、整数オーバーフローのチェックをコンパイラが入れ込みます。 プログラムがエラーとともに終了するとき、Rustはパニックという用語を使用します; パニックについては第9章の「panic!で回復不能なエラー」でより深く議論します。
--releaseフラグを付けてリリースモードでコンパイルしているときは、コンパイラはパニックを引き起こす整数オーバーフローチェックを入れ込みません。 代わりに、オーバーフローが起きたときは、プログラムは2の補数ラップアラウンド (two's complement wrapping) を行います。 一言で言うと、その型が取ることができる最大値よりも大きい値は、その型が取ることができる最小値に「回り込む」 (“wrap around”) のです。u8の場合は、値256は0になり、値257は1になり、という感じです。 プログラムはパニックはしなくなるでしょうが、変数が持っている値はおそらくプログラマが期待していたものではないでしょう。 整数オーバーフローのラップアラウンドの挙動に依存するのは、エラーと考えられます。オーバーフローが発生する可能性を明示的に取り扱うためには、プリミティブ数値型に関して標準ライブラリが提供する、以下のメソッド群を使うことができます:
wrapping_*メソッド(wrapping_add等)で、モードを問わずラップアラウンドさせる。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規格に従って表現されています。f32が単精度浮動小数点数、
f64が倍精度浮動小数点数です。
数値演算
Rustにも全数値型に期待されうる標準的な数学演算が用意されています: 足し算、引き算、掛け算、割り算、余りです。
整数の割り算では、0に近い方の最も近い整数に切り捨てられます。
以下の例では、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 // 結果は-1 // remainder // 余り let remainder = 43 % 5; }
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして、変数に束縛されます。 付録BにRustで使える演算子の一覧が載っています。
論理値型
他の多くの言語同様、Rustの論理値型も取りうる値は二つしかありません: trueとfalseです。
論理値のサイズは1バイトです。
Rustの論理値型は、boolと指定されます。
例です:
ファイル名: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation // 明示的型注釈付きで }
論理値を使う主な手段は、条件式です。例えば、if式などですね。if式のRustでの動作方法については、
「制御フロー」節で講義します。
文字型
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バイトのサイズを持ち、ユニコードのスカラー値を表します。これはつまり、アスキーよりもずっとたくさんのものを表せるということです。
アクセント記号付き文字; 中国語、日本語、韓国語の文字;
絵文字; ゼロ幅スペースはすべて、Rustでは有効なchar値です。ユニコードスカラー値は、
U+0000からU+D7FFまでとU+E000からU+10FFFFまでの範囲になります。
ところが、「文字」は実はユニコードの概念ではないので、文字とは何かという人間としての直観は、
Rustにおけるchar値が何かとは合致しない可能性があります。この話題については、第8章の「文字列でUTF-8でエンコードされたテキストを保持する」で詳しく議論しましょう。
複合型
複合型により、複数の値を一つの型にまとめることができます。Rustには、 2種類の基本的な複合型があります: タプルと配列です。
タプル型
タプルは、様々な型の複数の値を一つの複合型にまとめ上げる汎用的な手段です。 タプルの長さは固定です: 一度宣言されたらサイズは伸縮できません。
タプルは、丸かっこの中にカンマ区切りの値リストを書くことで生成します。タプルの位置ごとに型があり、 タプル内の値はそれぞれ全てが同じ型である必要はありません。今回の例では、型注釈をあえて追加しました:
ファイル名: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
変数tupは、タプル全体に束縛されています。なぜなら、タプルは、一つの複合要素と考えられるからです。
タプルから個々の値を取り出すには、パターンマッチングを使用して分解することができます。以下のように:
ファイル名: 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ですね)に変換しています。
この過程は、分配と呼ばれます。単独のタプルを破壊して三分割しているからです。最後に、
プログラムは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です。
値をひとつも持たないタプルはユニットという特別な名前を持っています。
この値と、それに対応する型はともに()と書き表され、空の値や空の戻り値型を表現します。
式は、特に値を返さなければ、暗黙的にユニット値を返します。
配列型
配列によっても、複数の値のコレクションを得ることができます。タプルと異なり、配列の全要素は、 同じ型でなければなりません。一部の他の言語の配列と異なり、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という名前の配列は3という値が5つあるものです。これはlet a = [3, 3, 3, 3, 3];と書くのと同じですが、より簡潔になります。
配列の要素にアクセスする
配列は、あらかじめ知られた固定サイズを持ち、スタック上に確保することができる一塊のメモリです。 添え字によって、配列の要素にこのようにアクセスすることができます:
ファイル名: src/main.rs
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}");
// {index}番目の要素の値は{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
(スレッド'main'はsrc/main.rs:19:19でパニックしました:
範囲外アクセス: 長さは5ですが、添え字は10でした)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
プログラムは、添え字アクセスで無効な値を使用した時点で実行時エラーに陥りました。
プログラムはエラーメッセージとともに終了し、最後のprintln!文を実行しませんでした。
要素に添え字アクセスを試みると、言語は、指定されたその添え字が配列長よりも小さいかを確認してくれます。
添え字が配列長と等しいかより大きければ、言語はパニックします。
この例の場合は特にそうですが、このチェックは実行時に行われなくてはなりません。
なぜならコンパイラは、ユーザが後でコードを実行したときに、ユーザがどんな値を入力するか知りようがないからです。
これは、実際に稼働しているRustのメモリ安全機構の例のひとつになります。低レベル言語の多くでは、 この種のチェックは行われないため、間違った添え字を与えると、無効なメモリにアクセスできてしまいます。 Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、 この種のエラーからプログラマを保護しています。第9章ではRustのエラー処理について、そして、 可読性が高く安全で、パニックもしなければ不正なメモリアクセスも許さないコードをどうすれば書けるのか、もっと議論します。