// book chapter
動かして学ぶRustプログラミング問題集第2章 基本の型と演算.
この章では、Rustが最初から用意している基本の型と、それらを使った演算を学びます。
整数・小数・真偽値・文字といった型の使い分けから、四則演算・キャストまでを10問で身につけます。
第1章では変数に型を書かずに済ませてきましたが、Rustはすべての値の型がコンパイル時に決まる言語です。この章では型を明示的に書く場面が増えます。
進め方は第1章と同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 整数型
整数型とプリミティブ型に関する問題です。
イベントの参加者数と冷凍庫の設定温度を、それぞれ変数に束縛して出力してください。
参加者数は負にならないので符号なしのu32を、設定温度は氷点下になるので符号ありのi32を、型注釈として付けてください。
参加者は42人です。
設定温度は-18度です。fn main() {
// 参加者数を表す変数participantsを、u32の型注釈付きで42として宣言せよ
// 設定温度を表す変数freezer_tempを、i32の型注釈付きで-18として宣言せよ
println!("参加者は{}人です。", participants);
println!("設定温度は{}度です。", freezer_temp);
}Playgroundで開く解答例と解説
fn main() {
// 参加者数を表す変数participantsを、u32の型注釈付きで42として宣言せよ
let participants: u32 = 42;
// 設定温度を表す変数freezer_tempを、i32の型注釈付きで-18として宣言せよ
let freezer_temp: i32 = -18;
println!("参加者は{}人です。", participants);
println!("設定温度は{}度です。", freezer_temp);
}Playgroundで開く変数の型を明示するときはlet 変数名: 型 = 値;と書きます。第1章で定数をconst TICKET_PRICE: u32 = 1200;と書いたのと同じ形式です。定数と違い、letの型注釈は省略できます(省略した場合はコンパイラが値から型を推論します)。
Rustの整数型は「符号あり/なし」と「ビット幅」の組み合わせで、i8〜i128・u8〜u128などが用意されています。uで始まる符号なし整数は0以上の値しか扱えないため、負の値がありえない個数や人数に向いています。
型注釈を省略した整数リテラルはi32になります。第1章でlet money = 1000;と書いてきたものは、すべてi32となっています。迷ったらまずi32を選び、負の値を取らないならu32、というのが基本的な使い分けになります。
02 - 整数型の範囲
整数型に関する問題です。
次のコードは1年の日数を変数に束縛しようとしていますが、コンパイルエラーになります。エラーメッセージを読み、型注釈だけを変えてコンパイルが通るようにしてください。
1年は365日です。fn main() {
let days_in_year: u8 = 365; // この行でコンパイルエラーになる
println!("1年は{}日です。", days_in_year);
}Playgroundで開く解答例と解説
fn main() {
let days_in_year: u8 = 365; // この行でコンパイルエラーになる
let days_in_year: u16 = 365;
println!("1年は{}日です。", days_in_year);
}Playgroundで開くエラーメッセージは「literal out of range for u8」、続けて「the literal 365 does not fit into the type u8 whose range is 0..=255」でした。つまり「365はu8の範囲0〜255に収まらない」という指摘です。
整数型はビット幅ごとに扱える値の範囲が決まっています。
| 型 | 扱える範囲 |
|---|---|
u8 |
0 〜 255 |
i8 |
-128 〜 127 |
u16 |
0 〜 65,535 |
i16 |
-32,768 〜 32,767 |
365を入れるにはu16以上の幅が必要です。u32やi32でも当然コンパイルは通るので、そちらでも正解です。
なお、この問題のようにリテラルの値が範囲外だとコンパイル時に弾かれますが、実行時の計算結果が範囲を超えた場合(オーバーフロー)は別の挙動になります。詳しくは整数型の補足を参照してください。
03 - 浮動小数点型
浮動小数点型に関する問題です。
りんご1個の値段をf64で宣言し、3個買ったときの合計金額を出力してください。
りんご1個は128.5円です。
3個で385.5円です。fn main() {
// りんごの単価を表す変数priceを、f64の型注釈付きで128.5として宣言せよ
println!("りんご1個は{}円です。", price);
// priceを3倍した合計金額をtotalに束縛せよ
println!("3個で{}円です。", total);
}Playgroundで開く解答例と解説
fn main() {
// りんごの単価を表す変数priceを、f64の型注釈付きで128.5として宣言せよ
let price: f64 = 128.5;
println!("りんご1個は{}円です。", price);
// priceを3倍した合計金額をtotalに束縛せよ
let total = price * 3.0;
println!("3個で{}円です。", total);
}Playgroundで開く小数を扱う型はf32とf64の2つで、整数型と違って符号なし版はありません。型注釈を省略した小数リテラルはf64になります。
ここでのポイントはprice * 3.0の3.0です。price * 3と書くとコンパイルエラーになります。Rustはf64と整数を暗黙に変換してくれないため、小数と掛け合わせる値も小数リテラルで書く必要があります。この「型が違うと演算できない」という性質は問題06で改めて扱います。
04 - 四則演算
数値演算に関する問題です。
2つの変数aとbについて、加算・減算・乗算・除算の結果を4行で出力してください。
16 + 4 = 20
16 - 4 = 12
16 × 4 = 64
16 ÷ 4 = 4fn main() {
let a = 16;
let b = 4;
// aとbの加算・減算・乗算・除算の結果を、数値演算に置き換えよ
println!("{} + {} = {}", a, b, 20);
println!("{} - {} = {}", a, b, 12);
println!("{} × {} = {}", a, b, 64);
println!("{} ÷ {} = {}", a, b, 4);
}Playgroundで開く解答例と解説
fn main() {
let a = 16;
let b = 4;
// aとbの加算・減算・乗算・除算の結果を、数値演算に置き換えよ
println!("{} + {} = {}", a, b, 20);
println!("{} - {} = {}", a, b, 12);
println!("{} × {} = {}", a, b, 64);
println!("{} ÷ {} = {}", a, b, 4);
println!("{} + {} = {}", a, b, a + b);
println!("{} - {} = {}", a, b, a - b);
println!("{} × {} = {}", a, b, a * b);
println!("{} ÷ {} = {}", a, b, a / b);
}Playgroundで開く四則演算の演算子は+(加算)・-(減算)・*(乗算)・/(除算)です。多くの言語と同じ書き方で、*は×ではなくアスタリスク、/は÷ではなくスラッシュを使います。
println!のプレースホルダには変数だけでなくa + bのような式を直接書けます。式は評価されてから埋め込まれるため、計算結果を一度変数に束縛する必要はありません。
なお、Rustには++や--(インクリメント・デクリメント)はありません。1増やしたいときはa += 1と書きます。
05 - 整数の除算と剰余
数値演算に関する問題です。
27人の生徒を1班4人ずつに分けます。できあがる班の数と、どの班にも入りきらずに余る人数を求めてください。
4人ずつの班が6班でき、3人余ります。fn main() {
let students = 27;
let per_group = 4;
// できあがる班の数をgroupsに束縛せよ
// 余る人数をremainderに束縛せよ
println!(
"{}人ずつの班が{}班でき、{}人余ります。",
per_group, groups, remainder
);
}Playgroundで開く解答例と解説
fn main() {
let students = 27;
let per_group = 4;
// できあがる班の数をgroupsに束縛せよ
let groups = students / per_group;
// 余る人数をremainderに束縛せよ
let remainder = students % per_group;
println!(
"{}人ずつの班が{}班でき、{}人余ります。",
per_group, groups, remainder
);
}Playgroundで開く27 / 4は算数では6.75ですが、整数同士の除算では小数部分が切り捨てられて6になります。四捨五入ではなく切り捨てである点に注意してください。小数の結果が欲しい場合は、あらかじめf64などの浮動小数点型に揃えて計算します(問題06で扱います)。
%は剰余(あまり)を求める演算子です。27 % 4は3になります。「割り切れるかどうか」をn % 2 == 0のように判定する使い方もよく登場します。
06 - 異なる型同士は演算できない
キャストと数値演算に関する問題です。
次のコードは、合計金額を人数で割って1人あたりの金額を求めようとしていますが、コンパイルエラー(E0277)になります。エラーメッセージを読み、totalとpeopleの宣言はそのままに、計算している行だけを修正してください。
1人あたり240円です。fn main() {
let total: f64 = 1200.0;
let people: i32 = 5;
let per_person = total / people; // エラー: E0277
println!("1人あたり{}円です。", per_person);
}Playgroundで開く解答例と解説
fn main() {
let total: f64 = 1200.0;
let people: i32 = 5;
let per_person = total / people; // エラー: E0277
let per_person = total / people as f64; // asでf64に揃えてから割る
println!("1人あたり{}円です。", per_person);
}Playgroundで開くエラーメッセージは「cannot divide f64 by i32」、つまり「f64をi32で割ることはできない」でした。
多くの言語では、整数と小数を混ぜて計算すると自動的に型が揃えられます。しかしRustは暗黙の型変換を一切行いません。同じ整数同士でもi32とi64のようにビット幅が違えばエラーになります。予期しない精度の低下や値の変化を防ぐための設計です。
型を揃えるには値 as 型と書きます。これをキャストと呼びます。ここではpeople as f64で5を5.0として扱い、f64同士の除算にしています。
なおtotal as i32 / peopleと書くと1200 / 5の整数除算になり、今回はたまたま同じ240になりますが、割り切れない場合は切り捨てられてしまいます。金額を小数のまま扱いたいならf64側に揃えるのが適切です。
07 - キャストの挙動
キャストに関する問題です。
次のコードには3つのキャストが登場します。それぞれ何が出力されるかを実行する前に予測し、それからPlaygroundで実行して答え合わせをしてください。
fn main() {
let score = 78.9;
println!("A: {}", score as i32);
let big = 300;
println!("B: {}", big as u8);
let passed = true;
println!("C: {}", passed as i32);
}Playgroundで開く答えと解説
出力は次のようになります。
A: 78
B: 44
C: 1
1つずつ見ていきましょう。
A: 浮動小数点型から整数型
78.9 as i32は78です。四捨五入ではなく、小数部分を0の方向へ切り捨てます。-78.9 as i32なら-78になります。
B: 大きい整数型から小さい整数型
300 as u8は44です。u8の範囲は0〜255なので300は収まりませんが、キャストはエラーにもパニックにもならず、上位ビットを切り捨てた結果(300 - 256 = 44)が黙って返ります。問題02では範囲外のリテラルがコンパイルエラーになりましたが、asを書くと「承知のうえで変換している」とみなされ、警告なしに値が変わってしまいます。
C: 論理値型から整数型
true as i32は1です(falseなら0)。boolから整数へのキャストは許可されていますが、逆方向の「整数からbool」はできません。
このようにasは失敗を報告してくれない変換です。値が確実に収まると分かっている場合や、切り捨てを意図している場合に限って使うのが安全です。
08 - 論理値型
論理値型に関する問題です。
在庫数から「在庫があるか」と「再入荷が必要か」を判定し、その結果をbool型の変数に束縛して出力してください。
在庫あり: false
再入荷が必要: truefn main() {
let stock = 0;
// stockが0より大きいかどうかの判定結果を、bool型の型注釈付きでin_stockに束縛せよ
// stockが0と等しいかどうかの判定結果を、bool型の型注釈付きでneeds_restockに束縛せよ
println!("在庫あり: {}", in_stock);
println!("再入荷が必要: {}", needs_restock);
}Playgroundで開く解答例と解説
fn main() {
let stock = 0;
// stockが0より大きいかどうかの判定結果を、bool型の型注釈付きでin_stockに束縛せよ
let in_stock: bool = stock > 0;
// stockが0と等しいかどうかの判定結果を、bool型の型注釈付きでneeds_restockに束縛せよ
let needs_restock: bool = stock == 0;
println!("在庫あり: {}", in_stock);
println!("再入荷が必要: {}", needs_restock);
}Playgroundで開くboolはtrueとfalseの2つの値だけを取る型です。>や==といった比較演算子はboolの値を生み出す式なので、その結果をそのまま変数に束縛できます。
比較演算子は==(等しい)・!=(等しくない)・<・<=・>・>=の6つです。代入の=と等値比較の==は別物なので混同しないよう注意してください。
ここでは学習のために: boolと型注釈を書きましたが、比較の結果がboolであることはコンパイラにも自明なので、実際のコードではlet in_stock = stock > 0;と省略するのが一般的です。
09 - 論理演算子
論理演算子と論理値型に関する問題です。
遊園地のアトラクションに乗れるのは、次の条件をすべて満たす場合です。
- 雨が降っていない
- 身長が140cm以上
- 12歳以上、または大人の同伴がある
この条件を1つの式にまとめて、判定結果を出力してください。
乗車できる: true
雨天時に乗車できる: falsefn main() {
let height = 145; // 身長(cm)
let age = 10;
let with_adult = true; // 大人の同伴があるか
let is_rainy = false;
// 上の3つの条件をすべて満たすかどうかをcan_rideに束縛せよ
println!("乗車できる: {}", can_ride);
// 雨が降ってきた
let is_rainy = true;
// 同じ条件で判定し直した結果をcan_ride_in_rainに束縛せよ
println!("雨天時に乗車できる: {}", can_ride_in_rain);
}Playgroundで開く解答例と解説
fn main() {
let height = 145; // 身長(cm)
let age = 10;
let with_adult = true; // 大人の同伴があるか
let is_rainy = false;
// 上の3つの条件をすべて満たすかどうかをcan_rideに束縛せよ
let can_ride = !is_rainy && height >= 140 && (age >= 12 || with_adult);
println!("乗車できる: {}", can_ride);
// 雨が降ってきた
let is_rainy = true;
// 同じ条件で判定し直した結果をcan_ride_in_rainに束縛せよ
let can_ride_in_rain = !is_rainy && height >= 140 && (age >= 12 || with_adult);
println!("雨天時に乗車できる: {}", can_ride_in_rain);
}Playgroundで開く論理演算子は&&(かつ)・||(または)・!(否定)の3つです。!is_rainyは「雨が降っていない」、つまりis_rainy == falseと同じ意味になります。
括弧の位置が重要です。&&は||より優先順位が高いため、age >= 12 || with_adultを括弧で囲まないと「12歳以上」の条件だけが手前の&&に結び付いてしまい、意図と違う判定になります。優先順位を覚えるより、括弧で明示するほうが読み手にも親切です。
1人目の判定は「雨ではない(true)」「145cm ≧ 140cm(true)」「10歳は12歳未満だが同伴あり(true)」がすべて成立してtrueになります。
2つ目のポイントはis_rainyのシャドーイングです。let is_rainy = true;で新しいis_rainyを作っても、すでに評価済みのcan_rideの値は変わりません。式は書いた時点で評価され、その結果が変数に束縛されるためです。
10 - 文字型
文字型と文字列リテラルに関する問題です。
次のコードは成績評価の1文字を変数に束縛しようとしていますが、コンパイルエラー(E0308)になります。エラーメッセージを読み、gradeの型注釈は変えずに修正してください。
成績評価はAです。fn main() {
let grade: char = "A"; // エラー: E0308
println!("成績評価は{}です。", grade);
}Playgroundで開く解答例と解説
fn main() {
let grade: char = "A"; // エラー: E0308
let grade: char = 'A'; // シングルクォートで囲む
println!("成績評価は{}です。", grade);
}Playgroundで開くエラーメッセージは「expected char, found &str」、つまり「charが入るはずの変数に文字列が入っている」という型の不一致です。
Rustではクォートの種類で型が変わります。
| 書き方 | 型 | 意味 |
|---|---|---|
'A' |
char |
1文字 |
"A" |
&str |
文字列(たまたま1文字のもの) |
第1章から使ってきた"Hello, world!"のようなダブルクォートの文字列リテラルは、1文字であっても文字列です。1文字を表すcharにはシングルクォートを使います。
charが表すのは「Unicodeスカラー値」1つなので、'あ'や'🦀'のような文字も1つのcharとして扱えます。逆に、シングルクォートの中に2文字以上を書くとコンパイルエラーになります。
これで第2章は終わりです。基本の型と演算がそろったので、次の章ではこれらを使って処理の流れを分岐・反復させる制御フローに進みます。