Day 002 — 基本型とデータ型

2026-07-30 🟢 初心者 / Phase 1 実装 基本型とデータ型

📚 背景知識(読んでから問題へ)

Rustの型は大きくスカラー型(1つの値を表す型: 整数・浮動小数点数・真偽値・文字)と複合型(複数の値をまとめる型: タプル・配列)に分かれます。C++やGoに慣れているとこの分類自体は馴染みがありますが、Rustには「所有権システムの安全性」を支えるための細かい設計判断がいくつも埋め込まれています。

スカラー型

  • 整数型: i8i128isize(符号あり)、u8u128usize(符号なし)。ビット幅を型名に明示するのはGoのint8/uint8と同じ発想ですが、Rustでは型推論で用途に合わせた型を選ばないとコンパイルエラーになる場面が多く、曖昧な暗黙変換がありません。
  • 浮動小数点型: f32f64(デフォルトはf64)。
  • 真偽値型: booltrue/falseのみ。C言語のような0/1との暗黙変換はありません)。
  • 文字型: char常に4バイトのUnicodeスカラー値です。C/C++のchar(1バイト)と混同しやすい落とし穴です。

整数のオーバーフロー

Rustの整数演算は、デバッグビルドではオーバーフロー時にpanicし、リリースビルドではラップアラウンド(2の補数での折り返し)します。ビルド設定によって挙動が変わるのは危険に思えるかもしれませんが、これは「開発中は早期に不具合を検知し、本番では余計なチェックコストを払わない」というRustの実用主義的な設計です。挙動を明示的に制御したい場合はchecked_addOptionで結果を返す)・wrapping_add(常にラップアラウンド)・saturating_add(上限/下限に張り付く)などのメソッドを使います。

複合型: タプルと配列

  • タプル (T1, T2, ...): 異なる型の値を固定個数まとめられます。名前を付けるほどでもない一時的なグルーピング(関数から複数の値を返す等)に向いています。
  • 配列 [T; N]: 同じ型の値をコンパイル時に確定した個数だけまとめます。サイズが型の一部([i32; 5][i32; 6]は別の型)なので、コンパイラはスタック上に必要なサイズを正確に確保でき、実行時のメモリ確保(ヒープアロケーション)が発生しません。これは「ゼロコスト抽象化」の一例です。可変長が必要な場合はVec<T>(Day 11で学習)を使います。

📝 問題

以下の要件を満たすmain.rsを実装してください。

  1. スカラー型: i32scoref64ratioboolis_passedchargradeを宣言し、まとめて出力する
  2. 整数オーバーフロー: u8型の変数near_max255を代入し、checked_add(1)で安全に加算を試みる。結果がNoneなら「オーバーフローしました」と出力する。さらにwrapping_add(1)を使った場合の結果(0になること)も出力する
  3. タプル: 名前(&str)・年齢(u32)・身長(f64)を保持するタプルpersonを作り、分割代入(destructuring)で3つの変数に取り出して出力する。さらに、[i32; 5]型の配列への参照を受け取り(i32, i32)(最小値, 最大値)のタプルを返す関数min_max(arr: &[i32; 5]) -> (i32, i32)を実装する
  4. 配列とスライス: [i32; 5]型の固定長配列numbersを作り、for文で合計を計算して出力する。また配列の2番目から4番目までの要素をスライス&numbers[1..4]として取り出し、{:?}で出力する

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • スカラー型はすべて型注釈を明示すれば宣言できます。特別なことは不要です
  • checked_addOption<T>を返します。matchまたはif letSome/Noneを分岐できます
  • タプルはlet (a, b, c) = tuple;の形で分割代入できます
  • 配列の合計はforループで愚直に足し込めば十分です
ヒント2 — アプローチ
  • u8::checked_addは自分自身が符号なし8ビット整数なので、255u8.checked_add(1)Noneを返します
  • wrapping_addは常にSomeを経由せず直接値を返します(Optionではありません)
  • min_max関数の中では、まずarr[0]を初期値としてmin/maxの変数を用意し、for &v in arr.iter()でループしながら比較・更新します
  • 配列のスライスは&numbers[1..4]のように書けます。範囲1..4は「開始を含み終了を含まない」(1, 2, 3番目のインデックス)ことに注意してください
  • {:?}(Debugフォーマット)はスライスや配列をそのまま出力するのに使えます
ヒント3 — コード骨格
fn min_max(arr: &[i32; 5]) -> (i32, i32) {
    let mut min = arr[0];
    let mut max = arr[0];
    for &v in arr.iter() {
        // minとmaxを更新...
    }
    (min, max)
}

fn main() {
    // 1. スカラー型
    let score: i32 = 88;
    let ratio: f64 = 0.75;
    let is_passed: bool = true;
    let grade: char = 'A';
    // 出力...

    // 2. 整数オーバーフロー
    let near_max: u8 = 255;
    // checked_add と wrapping_add ...

    // 3. タプル
    let person: (&str, u32, f64) = ("Alice", 30, 165.5);
    let (name, age, height) = person;
    // 出力...

    let numbers: [i32; 5] = [4, 8, 15, 16, 23];
    let (min, max) = min_max(&numbers);
    // 出力...

    // 4. 配列とスライス
    // 合計を計算し、スライスを出力...
}

模範解答

fn min_max(arr: &[i32; 5]) -> (i32, i32) {
    let mut min = arr[0];
    let mut max = arr[0];
    for &v in arr.iter() {
        if v < min {
            min = v;
        }
        if v > max {
            max = v;
        }
    }
    (min, max)
}

fn main() {
    // --- 1. スカラー型 ---
    let score: i32 = 88;
    let ratio: f64 = 0.75;
    let is_passed: bool = true;
    let grade: char = 'A';
    println!("--- スカラー型 ---");
    println!(
        "score: {}, ratio: {}, is_passed: {}, grade: {}",
        score, ratio, is_passed, grade
    );

    // --- 2. 整数オーバーフロー ---
    let near_max: u8 = 255;
    println!("--- 整数オーバーフロー ---");
    match near_max.checked_add(1) {
        Some(v) => println!("checked_add: {}", v),
        None => println!("checked_add: オーバーフローしました"),
    }
    let wrapped = near_max.wrapping_add(1);
    println!("wrapping_add: {}", wrapped);

    // --- 3. タプル ---
    let person: (&str, u32, f64) = ("Alice", 30, 165.5);
    let (name, age, height) = person;
    println!("--- タプル ---");
    println!("name: {}, age: {}, height: {}", name, age, height);

    let numbers: [i32; 5] = [4, 8, 15, 16, 23];
    let (min, max) = min_max(&numbers);
    println!("min: {}, max: {}", min, max);

    // --- 4. 配列とスライス ---
    let mut sum = 0;
    for v in numbers.iter() {
        sum += v;
    }
    println!("--- 配列とスライス ---");
    println!("合計: {}", sum);
    let middle = &numbers[1..4];
    println!("スライス [1..4]: {:?}", middle);
}
▶ 実行結果を見る(cargo run)
--- スカラー型 ---
score: 88, ratio: 0.75, is_passed: true, grade: A
--- 整数オーバーフロー ---
checked_add: オーバーフローしました
wrapping_add: 0
--- タプル ---
name: Alice, age: 30, height: 165.5
min: 4, max: 23
--- 配列とスライス ---
合計: 66
スライス [1..4]: [8, 15, 16]

🪜 Step-by-Step 解説

1
スカラー型を宣言し出力する
let score: i32 = 88;
let ratio: f64 = 0.75;
let is_passed: bool = true;
let grade: char = 'A';
いずれも型注釈を明示していますが、右辺のリテラルから型推論できる場合は省略可能です。charはシングルクォート'A'で表現し、ダブルクォート"A"&str)とは別物である点に注意します。
2
checked_addwrapping_addでオーバーフローを制御する
let near_max: u8 = 255;
match near_max.checked_add(1) {
    Some(v) => println!("checked_add: {}", v),
    None => println!("checked_add: オーバーフローしました"),
}
let wrapped = near_max.wrapping_add(1);
u8の最大値は255なので、255 + 1は表現できません。checked_addはこれを検出してNoneを返し、wrapping_addは2の補数のルールで折り返して0を返します。どちらのメソッドを使うかは「オーバーフローをエラーとして扱いたいか、意図した仕様として扱いたいか」で選びます。
3
タプルの分割代入と、タプルを返す関数
let person: (&str, u32, f64) = ("Alice", 30, 165.5);
let (name, age, height) = person;
タプルは.0, .1, .2のようにインデックスでもアクセスできますが、分割代入で意味のある名前を付けた方が可読性が上がります。
fn min_max(arr: &[i32; 5]) -> (i32, i32) {
    let mut min = arr[0];
    let mut max = arr[0];
    for &v in arr.iter() {
        if v < min { min = v; }
        if v > max { max = v; }
    }
    (min, max)
}
arr: &[i32; 5]は「サイズ5固定のi32配列への参照」です。arr.iter()&i32を順に返すので、for &v in ...のパターンでデリファレンスし、vi32として扱っています。
4
配列の合計とスライス
let mut sum = 0;
for v in numbers.iter() {
    sum += v;
}
let middle = &numbers[1..4];
numbers.iter()は各要素への参照を返しますが、sum += vのように数値演算に使う場合はRustの数値型の演算子オーバーロードが参照越しでも動作するため、明示的なデリファレンスなしで書けます。&numbers[1..4]は「配列そのものをコピーせず、範囲を指す軽量なビュー(スライス)」を作る操作で、実データのコピーは発生しません。

💡 設計思想・なぜこう書くのか

📌
配列のサイズが型の一部である理由: [i32; 5][i32; 6]を別の型として扱うことで、コンパイラは実行前にスタック上の必要サイズを正確に計算できます。C言語の配列はポインタに減衰(decay)してサイズ情報を失いますが、Rustは型システムにサイズを保持させることで「範囲外アクセスをコンパイル時・実行時双方でより安全にチェックできる」土台を作っています。
📌
タプルと配列を分けている理由: タプルは「異なる型を固定個数まとめる」、配列は「同じ型を固定個数まとめる」という役割分担がされています。これにより、配列に対してはforループやiter()のような「均一な要素に対する反復処理」が型システムレベルで安全に提供でき、タプルに対しては「決まった個数・決まった位置の値の取り出し」という異なる操作が自然に表現できます。
📌
デバッグ/リリースでオーバーフロー挙動が変わる理由: 開発中はオーバーフローという「意図しないバグの兆候」を即座にpanicで検知したい一方、本番のリリースビルドでは毎回オーバーフローチェックを行うコストを払いたくありません。Rustはoverflow-checksというビルド設定でこれを制御可能にし、デフォルトでは開発の安全性と本番の性能の両方を満たす折衷案を採用しています。曖昧な挙動に頼りたくない場面ではchecked_*/wrapping_*/saturating_*を明示的に使うことで、ビルド設定に依存しない一貫した挙動を保証できます。

🛑 コンパイルエラーが出た場合

配列は同じ型の要素しか持てません。次のように異なる型を混在させるとコンパイルエラーになります。

fn main() {
    let mixed = [1, "two", 3];
}
error[E0308]: mismatched types --> src/main.rs:2:21 | 2 | let mixed = [1, "two", 3]; | ^^^^^ expected integer, found `&str`

読み方:

  • error[E0308]: 型の不一致エラー。配列の最初の要素1から「この配列は整数型の配列だ」と推論された後、2番目の要素"two"&str)が矛盾している
  • --> src/main.rs:2:21: エラーが発生した場所(2行目21文字目、"two"の開始位置)
  • expected integer, found `&str`: 期待した整数型ではなく&strが来ていることを指摘している

修正方法: 異なる型を1つにまとめたい場合は配列ではなくタプル(1, "two", 3)を使います。配列はあくまで「同じ型の値の並び」専用の型です。

🌐 他言語との比較

観点RustGoJavaTypeScript
固定長・同一型の配列[T; N]。サイズが型の一部でスタック確保[N]T。値型で挙動はRustに近いT[]。常にヒープ上の参照型T[]。可変長で内部は配列オブジェクト。固定長は型レベルの制約のみ
異なる型をまとめる(タプル)(T1, T2, ...)がネイティブタプル型なし。複数戻り値か構造体で代替タプル型なし。レコードやPairクラスで代替[T1, T2]という型注釈(実体はArray)
整数オーバーフロー時の挙動debug: panic / release: ラップアラウンド。checked_*等で明示制御可常にsilentにラップアラウンド常にsilentにラップアラウンド(int/long)数値はdouble表現のため、整数オーバーフローより先に精度落ちが問題になる
文字型charは4バイトのUnicodeスカラー値runeは4バイト(int32のエイリアス)、byteは1バイトcharは2バイトのUTF-16コード単位専用のchar型はなくstringの1文字として扱う

Rustが際立つのは、配列のサイズを型システムに組み込んでいる点と、オーバーフロー時の挙動をchecked_*/wrapping_*/saturating_*で明示的に選べる点です。Go/Javaは「常にラップアラウンド」で固定されているため、意図しないオーバーフローに気づきにくいという弱点があります。

🏆 実務での使いどころ

  • 固定サイズの設定値配列: 曜日名の一覧、HTTPステータスコードのテーブルなど、要素数が実行時に変わらないデータはVecより[T; N]の方がヒープ確保のオーバーヘッドがなく高速です
  • 複数戻り値としてのタプル: 座標(x, y)や集計結果の(min, max)など、2〜3個の値を一時的にまとめて返す場面で使われます。フィールドが増えたり意味が重要になったりする場合は、タプルではなく名前付きフィールドを持つ構造体(Day 6)に切り替えるべきタイミングです
  • 金額計算でのオーバーフロー対策: 決済・会計システムなど、オーバーフローが金銭的な事故に直結するドメインではchecked_add/checked_subを使い、Noneが返った時点で処理を中断するのが実務での定石です
  • UTF-8とcharの違いを意識した文字列処理: 日本語や絵文字などマルチバイト文字を扱う際、char単位の反復(.chars())とバイト単位の反復(.bytes())の違いを理解していないと、文字列の途中で予期しない位置にアクセスしてpanicする不具合を招きます

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
配列とVecを同じものだと思う他言語の配列(Go/TSのスライス的な配列)は可変長がデフォルトのためRustの[T; N]は固定長でコンパイル時にサイズが確定する。可変長が必要ならVec<T>(Day 11)を使う
タプルは3要素以上でも気軽に使ってよいと思うPythonなど他言語でタプルを多用する習慣があるためフィールド数が増えたり意味が不明瞭になったりする場合は、タプルではなく名前付きフィールドを持つ構造体を使うべき。タプルは2〜3要素の一時的なグルーピングに留める
整数オーバーフローはRustでは常にpanicすると思うデバッグビルドでのpanicしか経験していないためリリースビルドではデフォルトでラップアラウンドする。挙動をビルド設定に依存させたくない場合はchecked_*/wrapping_*/saturating_*を明示的に使う
charは1バイトだと思う(C言語の癖)C/C++のcharが1バイトであるためRustのcharは常に4バイトのUnicodeスカラー値。UTF-8のバイト列とは別物で、文字列へのバイト単位のインデックスアクセス(s[i]のような操作)は意図的に許可されていない

🚀 次のステップ

  • 発展: 同じデータをVec<i32>で持った場合とどう違うかを実際に触ってみましょう。要素数を実行時に変えられること、ヒープ確保が発生することを体感すると、Day 11(Vecの基礎)の理解がスムーズになります
  • 次回予告: Day 003 — 所有権の基礎(ムーブセマンティクス)(値のムーブとCopy/Cloneの違い)

🎯 自己評価

自分の回答

気づき・メモ