[Rust 入門] #02 変数・型・関数 — let と mut、整数の型、式と文を Java と並べて読む

[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
JavaRust
書き換えられない変数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 とは、順序が逆です。

JavaRust
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 は符号なし)と、ビット数で決まります。

符号あり符号なしビット数
i8u88
i16u1616
i32u3232
i64u6464
i128u128128
isizeusizeCPU のビット数(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 u844上のビットが切り捨てられる(300 − 256 = 44)
-1_i32 as u324294967295ビットの並びが、そのまま符号なしとして読まれる
3.99_f64 as i323小数点以下が切り捨てられる
300.0_f64 as u8255範囲を超えると、最大値で止まる

整数どうしは、切り捨てで、小数から整数は、範囲で止まるので、動きが違います。同じ 300 を u8 にしても、整数からは 44、小数からは 255 になります。

Java で同じ値を変換した結果は、次のとおりでした。

Java結果
(byte) 300(int)44
(byte) 300.0(double)44
(int) 3.993
(int) 3e102147483647

(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 --release0(桁あふれして、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..41, 2, 3(終わりを含まない)
1..=41, 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  暑い

使っている要素は、次のとおりです。

要素該当する箇所
constSTART / END / STEP
型の変換c as f64(i32 から f64 へ)
戻り値のある関数c_to_f、label
式としての iflabel
let mut と whilemain
書式{:>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回)

  1. Rust とは / 環境構築 / Hello World
  2. 変数・型・関数(今回)
  3. 所有権(ムーブ)
  4. 借用と参照
  5. 構造体・enum・match
  6. Result / Option とエラー処理
  7. トレイトとジェネリクス(Java の interface との違い)
  8. ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)