[Rust 入門] #02 変数・型・関数 — let と mut、整数の型、式と文を Java と並べて読む
はじめに
第1回の最後に、変数を書き換えようとして、コンパイルエラーになりました。
error[E0384]: cannot assign twice to immutable variable `x`
今回は、この let mut を入り口に、変数・型・関数を、Java と並べて読みます。第1回で println! の中に出てきた let や fn を、ここで1つずつ、説明します。
変数
let と mut
let x = 5;
let mut y = 5;
y = y + 1;
println!("x = {x}, y = {y}");
x = 5, y = 6
| Java | Rust | |
|---|---|---|
| 書き換えられない変数 | final int x = 5; | let x = 5; |
| 書き換えられる変数 | int x = 5; | let mut x = 5; |
Java は、書き換えられるのが初期状態で、final を付けると、書き換えられなくなります。Rust は、反対です。書き換えられないのが初期状態で、mut を付けると、書き換えられます。
シャドーイング
同じ名前で、もう一度 let を書くと、新しい変数が作られて、前の変数は、隠れます。
let z = "42";
let z: i32 = z.parse().unwrap();
let z = z * 2;
println!("z = {z}");
z = 84
1行目の z は文字列、2行目の z は整数です。mut で書き換えるのと違って、型が変わっても構いません。文字列を数値に直すときに、z_str と z_num のように、名前を分けなくて済みます。
.parse().unwrap() は、文字列を数値に直す書き方です。unwrap の意味は、第6回(Result と Option)で扱います。
const
const MAX_HP: u32 = 100;
println!("MAX_HP = {MAX_HP}");
MAX_HP = 100
const は、型を必ず書く必要があります。名前は、大文字とアンダースコアで書くのが慣例です。Java の static final に近いものです。
型
型の書き方
型は、変数名の後ろに、: で区切って書きます。Java とは、順序が逆です。
| Java | Rust |
|---|---|
int a = -10; | let a: i32 = -10; |
double c = 3.14; | let c: f64 = 3.14; |
boolean d = true; | let d: bool = true; |
型は、省略もできます。値から決まるからです。
let i = 7;
let f = 2.5;
println!("{} {}", std::any::type_name_of_val(&i), std::any::type_name_of_val(&f));
i32 f64
整数だけ書くと i32、小数が付くと f64 になります。
整数
名前は、符号(i は符号あり、u は符号なし)と、ビット数で決まります。
| 符号あり | 符号なし | ビット数 |
|---|---|---|
i8 | u8 | 8 |
i16 | u16 | 16 |
i32 | u32 | 32 |
i64 | u64 | 64 |
i128 | u128 | 128 |
isize | usize | CPU のビット数(64 ビットの環境では 64) |
範囲は、次のとおりです。
println!("i8: {}..{}", i8::MIN, i8::MAX);
println!("u8: {}..{}", u8::MIN, u8::MAX);
println!("i32: {}..{}", i32::MIN, i32::MAX);
println!("u32: {}..{}", u32::MIN, u32::MAX);
i8: -128..127
u8: 0..255
i32: -2147483648..2147483647
u32: 0..4294967295
Java には、符号なしの整数型が、ありません(char だけは、符号なしの 16 ビットです)。Rust の u8 や u32 は、ファイルのバイト列や、サイズを扱うときに、そのまま使えます。
その他の型
let b: u8 = 255;
let c: f64 = 3.14;
let d: bool = true;
let e: char = 'あ';
println!("{b} {c} {d} {e}");
255 3.14 true あ
各型の大きさ(バイト)は、std::mem::size_of で測れます。
i32=4 u8=1 f64=8 bool=1 char=4 usize=8
char は 4 バイトです。Java の char(2 バイト)とは違い、Unicode の1文字が、そのまま入ります。
文字数とバイト数
println!("{}", 'あ'.len_utf8());
println!("{}", "あいう".len());
println!("{}", "あいう".chars().count());
3
9
3
| 書き方 | 結果 | 意味 |
|---|---|---|
'あ'.len_utf8() | 3 | ’あ’ を UTF-8 で表したときの、バイト数 |
"あいう".len() | 9 | 文字列のバイト数 |
"あいう".chars().count() | 3 | 文字の数 |
Java の "あいう".length() は、3 を返します。Rust の len() は、バイト数です。日本語を扱うときは、chars().count() を使います。
文字列の型(&str と String)は、第3回の所有権で扱います。
タプルと配列
let t: (i32, f64, char) = (1, 2.5, 'x');
println!("{} {} {} / {:?}", t.0, t.1, t.2, t);
let (p, q, r) = t;
println!("{p} {q} {r}");
let arr = [10, 20, 30];
println!("{} {} {:?} len={}", arr[0], arr[2], arr, arr.len());
let zeros = [0; 5];
println!("{:?}", zeros);
1 2.5 x / (1, 2.5, 'x')
1 2.5 x
10 30 [10, 20, 30] len=3
[0, 0, 0, 0, 0]
- タプル: 型の違う値を、まとめます。
t.0のように、番号で取り出します。let (p, q, r) = t;で、一度に分けることもできます - 配列: 同じ型の値を、決まった数だけ並べます。
[0; 5]は、0 を 5 個並べる書き方です
型の変換
暗黙の変換はない
Java は、int と long を足すと、自動で long になります。Rust は、自動では変換しません。
fn main() {
let a: i32 = 5;
let b: i64 = 10;
let c = a + b;
println!("{c}");
}
error[E0308]: mismatched types
--> err1.rs:4:17
|
4 | let c = a + b;
| ^ expected `i32`, found `i64`
error[E0277]: cannot add `i64` to `i32`
--> err1.rs:4:15
|
4 | let c = a + b;
| ^ no implementation for `i32 + i64`
|
= help: the trait `Add<i64>` is not implemented for `i32`
(以下、長い補足が続くため省略)
エラーは2つ出ますが、言っていることは同じです。i32 と i64 は、足せません。
as で変換する
型を変えたいときは、as で、書きます。
let big: i32 = 300;
println!("{} as u8 = {}", big, big as u8);
let neg: i32 = -1;
println!("{} as u32 = {}", neg, neg as u32);
let fl: f64 = 3.99;
println!("{} as i32 = {}", fl, fl as i32);
println!("{} as u8 = {}", 300.0_f64, 300.0_f64 as u8);
300 as u8 = 44
-1 as u32 = 4294967295
3.99 as i32 = 3
300 as u8 = 255
| 変換 | 結果 | 起きていること |
|---|---|---|
300_i32 as u8 | 44 | 上のビットが切り捨てられる(300 − 256 = 44) |
-1_i32 as u32 | 4294967295 | ビットの並びが、そのまま符号なしとして読まれる |
3.99_f64 as i32 | 3 | 小数点以下が切り捨てられる |
300.0_f64 as u8 | 255 | 範囲を超えると、最大値で止まる |
整数どうしは、切り捨てで、小数から整数は、範囲で止まるので、動きが違います。同じ 300 を u8 にしても、整数からは 44、小数からは 255 になります。
Java で同じ値を変換した結果は、次のとおりでした。
| Java | 結果 |
|---|---|
(byte) 300(int) | 44 |
(byte) 300.0(double) | 44 |
(int) 3.99 | 3 |
(int) 3e10 | 2147483647 |
(byte) 300.0 は、Java では 44、Rust の 300.0 as u8 は 255 です。ここは、そろっていません。ただし、byte は符号あり、u8 は符号なしなので、型が違います。同じ値を i8 にした比較は、していません。
整数のオーバーフロー
u8 の最大値 255 に、1 を足すと、どうなるでしょうか。
fn main() {
let x: u8 = 255;
let y = x + std::env::args().count() as u8;
println!("{y}");
}
(std::env::args().count() は、コマンドライン引数の数で、ここでは 1 になります。足す数をコンパイル時に決めないための、書き方です)
cargo run(debug)で実行すると、プログラムが止まります。
thread 'main' (655) panicked at src/main.rs:3:13:
attempt to add with overflow
終了コードは 101 でした。cargo run --release で実行すると、止まらずに、0 が出ます(終了コードは 0)。
0
| ビルド | 255u8 + 1 |
|---|---|
cargo run(debug) | パニック(attempt to add with overflow) |
cargo run --release | 0(桁あふれして、0 に戻る) |
rustc だけで試しても、同じでした。最適化なしの rustc はパニック、rustc -O は 0 です。
ビルドの種類で、結果が変わります。 Java の int は、常に桁あふれして、戻ります(Integer.MAX_VALUE + 1 は -2147483648)。
どちらの動きを使うか、書き方で決められます。
println!("{}", i32::MAX.wrapping_add(1));
println!("{:?}", i32::MAX.checked_add(1));
-2147483648
None
wrapping_add: 桁あふれして、戻る(Java のintと同じ結果)checked_add: 桁あふれするとNoneを返す(Optionは第6回)
割り算
整数どうしの割り算は、切り捨てです。符号つきの結果も、Java と同じでした。
println!("{} {} {}", 7 / 2, -7 / 2, -7 % 3);
println!("{}", 7.0 / 2.0);
3 -3 -1
3.5
Java の 7 / 2、-7 / 2、-7 % 3 も、同じく 3、-3、-1 でした。
関数
引数と戻り値
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn greet(name: &str) {
println!("こんにちは、{name}さん");
}
println!("{}", add(2, 3));
greet("lain");
5
こんにちは、lainさん
| 部分 | 意味 |
|---|---|
a: i32 | 引数。型を必ず書きます(Java と違って、型が後ろです) |
-> i32 | 戻り値の型。戻り値がなければ、-> ごと省略します |
a + b | 最後の式が、そのまま戻り値になります。; を付けません |
return a + b; と書いても、同じ結果になります(return を使った版も、動かして 5 を確かめました)。ただ、Rust では、最後の式を、return なしで書くのが、普通です。
greet の引数 &str は、文字列リテラルを渡すときの型です。String との違いは、第3回で扱います。
複数の値を返す
タプルを使うと、複数の値を返せます。
fn min_max(a: i32, b: i32) -> (i32, i32) {
if a < b { (a, b) } else { (b, a) }
}
let (lo, hi) = min_max(9, 4);
println!("{lo} {hi}");
4 9
式と文
Rust では、**値を返すもの(式)**と、**返さないもの(文)**が、はっきり分かれています。
let x = 5;は、文です。値を返しませんa + bは、式です。値を返します;を付けると、式は文になって、値が捨てられます
関数の最後に ; を付けてしまうと、どうなるでしょうか。
fn add(a: i32, b: i32) -> i32 {
a + b;
}
error[E0308]: mismatched types
--> err2.rs:1:27
|
1 | fn add(a: i32, b: i32) -> i32 {
| --- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | a + b;
| - help: remove this semicolon to return this value
i32 を返すはずなのに、()(何もない値)が返っている、と言っています。help が、「この ; を消せば、値が返る」と教えています。第1回の「セミコロンを忘れる」の、反対です。
if も式
if は、式です。値を返せるので、変数に入れられます。
let score = 72;
let grade = if score >= 80 { "A" } else if score >= 60 { "B" } else { "C" };
println!("{grade}");
B
Java の三項演算子(? :)にあたる書き方が、if だけで、書けます。波かっこ { } のブロックも、式です。最後の式が、ブロックの値になります。
let v = {
let a = 3;
let b = 4;
a * b
};
println!("{v}");
12
関数の戻り値に if をそのまま使うこともできます。
fn sign(n: i32) -> &'static str {
if n > 0 {
"正"
} else if n < 0 {
"負"
} else {
"ゼロ"
}
}
println!("{} {} {}", sign(5), sign(-3), sign(0));
正 負 ゼロ
戻り値の &'static str は、「プログラムの最初から最後まで有効な文字列」を表す型です。'static の意味は、ライフタイムを扱う第8回で見ます。今は、「文字列リテラルを返すときの型」と読んでおいてください。
繰り返し
loop / while / for
// loop: 無限ループ。break に値を付けると、loop の値になる
let mut n = 0;
let r = loop {
n += 1;
if n == 5 {
break n * 10;
}
};
println!("loop -> {r}");
// while
let mut k = 3;
while k > 0 {
print!("{k} ");
k -= 1;
}
println!("発射");
// for と範囲
for i in 1..4 {
print!("{i} ");
}
println!();
for i in 1..=4 {
print!("{i} ");
}
println!();
loop -> 50
3 2 1 発射
1 2 3
1 2 3 4
| 書き方 | 意味 |
|---|---|
loop { } | 無限ループ。break 値 で、ループの値を返せます |
while 条件 { } | 条件が成り立つ間、繰り返します |
1..4 | 1, 2, 3(終わりを含まない) |
1..=4 | 1, 2, 3, 4(終わりを含む) |
n += 1 のような複合代入は、Java と同じです。ただし、Java の n++ は、ありません。書くと、こう言われます。
error: Rust has no postfix increment operator
--> e5.rs:3:6
|
3 | n++;
| ^^ not a valid postfix operator
|
help: use `+= 1` instead
if や while の条件に、( ) は、要りません。条件は、bool でなければなりません。整数を書くと、エラーになります。
error[E0308]: mismatched types
--> e6.rs:3:8
|
3 | if n {
| ^ expected `bool`, found integer
配列を回す
let arr = [10, 20, 30];
for v in arr {
print!("{v} ");
}
println!();
for (i, v) in arr.iter().enumerate() {
print!("[{i}]={v} ");
}
println!();
10 20 30
[0]=10 [1]=20 [2]=30
for v in arr で、要素を順に取り出せます。番号も欲しいときは、.iter().enumerate() を使います。iter() や enumerate() の仕組みは、トレイトを扱う第7回のあとで、改めて見ます。
サンプル: 温度変換表
今回の内容を使って、摂氏から華氏への変換表を作ります。
cargo new temp
cd temp
src/main.rs を、次のようにします。
const START: i32 = -10;
const END: i32 = 40;
const STEP: i32 = 10;
fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn label(c: i32) -> &'static str {
if c <= 0 {
"氷点下"
} else if c < 25 {
"ふつう"
} else {
"暑い"
}
}
fn main() {
println!("{:>4} {:>7} メモ", "摂氏", "華氏");
let mut c = START;
while c <= END {
let f = c_to_f(c as f64);
println!("{:>4} {:>7.1} {}", c, f, label(c));
c += STEP;
}
}
cargo run
摂氏 華氏 メモ
-10 14.0 氷点下
0 32.0 氷点下
10 50.0 ふつう
20 68.0 ふつう
30 86.0 暑い
40 104.0 暑い
使っている要素は、次のとおりです。
| 要素 | 該当する箇所 |
|---|---|
const | START / END / STEP |
| 型の変換 | c as f64(i32 から f64 へ) |
| 戻り値のある関数 | c_to_f、label |
式としての if | label |
let mut と while | main |
| 書式 | {:>7.1} は、幅 7 で右寄せ、小数点以下 1 桁 |
c_to_f の計算は、c * 9.0 / 5.0 + 32.0 です。c は f64 なので、9.0 のように、小数の形で書きます。c * 9 / 5 + 32.0 と書くと、整数と小数が混ざって、コンパイルエラーになります。
error[E0277]: cannot multiply `f64` by `{integer}`
--> e4.rs:2:7
|
2 | c * 9 / 5 + 32.0
| ^ no implementation for `f64 * {integer}`
{:>4} は、日本語の文字も、1 文字を 1 桁として数えます。画面では、全角文字の幅が広いので、見出しの行は、少し、ずれて見えます。
cargo clippy と cargo fmt --check は、指摘なしで通りました。
動作確認
確認できたこと
- 上のコードと出力は、すべて、Linux(Rust 1.97.0、Cargo 1.97.0)で、実際に実行した結果です
- Java との比較は、同じ環境の Java で、実際に実行した結果です(
(byte) 300、(byte) 300.0、(int) 3.99、(int) 3e10、Integer.MAX_VALUE + 1、7 / 2、-7 / 2、-7 % 3、"あいう".length()) - 5つのエラー(
i32 + i64、関数末尾の;、n++、整数をifの条件にする、f64 * 整数)のメッセージ - オーバーフローは、
cargo runでパニック、cargo run --releaseで0になったこと。rustcとrustc -Oでも、同じ結果だったこと - 温度変換表のサンプルが、
cargo runで動くこと。cargo clippyとcargo fmt --checkが、指摘なしで通ること - 温度変換表のサンプルは、WSL の Ubuntu 24.04(Rust 1.99.1、
rustc main.rsで実行)でも、同じ出力になりました
確認できていないこと
isize/usizeの大きさは、64 ビットの環境(usize=8)でだけ、確認しました(byte) 300.0(Java)と300.0 as i8(Rust)のように、符号ありどうしでそろえた比較- Windows(ネイティブ)と macOS での実行結果。出力の内容は、同じになるはずですが、実行はしていません
次回
次回は、**所有権(ムーブ)**です。Rust で、いちばん最初に、つまずきやすいところです。let s = String::from("hello"); のあと、s を、別の変数に渡すと、何が起きるのかを、動かしながら見ていきます。
連載の目次(予定)
入門編(8回)
- Rust とは / 環境構築 / Hello World
- 変数・型・関数(今回)
- 所有権(ムーブ)
- 借用と参照
- 構造体・enum・match
- Result / Option とエラー処理
- トレイトとジェネリクス(Java の interface との違い)
- ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)