【Rust入門】構造体とEnumでデータを整理!match式で「バグらせない」ロジック構築術

前回はRust学習における「最大の壁」である所有権と借用について解説しました。
メモリ管理の厳しいルールに、少し頭が疲れてしまったかもしれませんね。

しかし、ここからがRustの楽しいところです!
今回は、バラバラのデータをひとまとめにして扱いやすくする「構造体(Struct)」と、Rustにおいて非常に強力な機能を持つ「列挙型(Enum)」、そしてそれらを安全に処理する「match式」について学びます。

これらを使いこなせると、プログラムの「見通し」が一気に良くなり、「コンパイルが通れば正しく動く」というRustの安心感をより強く感じられるようになりますよ。


今回のゴール

  1. 構造体を使って、関連するデータを一つにまとめる。
  2. Enumを使い、データの「種類」や「状態」を定義する(データを持てるEnumの強さを知る)。
  3. match式を使って、あらゆるパターンを漏れなく処理する堅牢なコードを書く。

1. 構造体(Struct):関連するデータをパッケージ化しよう

プログラミングをしていると、「名前」と「年齢」と「メールアドレス」のように、関連するデータをセットで扱いたい場面が必ず出てきます。
これを実現するのが構造体(Struct)です。

構造体の定義

構造体は、データの「設計図」のようなものです。

// ユーザー情報をまとめる構造体
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

インスタンスの作成と利用

設計図ができたら、実際にデータ(インスタンス)を作ってみましょう。

fn main() {
    // 構造体のインスタンスを作成
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    // ドット(.)を使ってデータにアクセス
    println!("ユーザー名: {}", user1.username);
    println!("メール: {}", user1.email);
}

データの変更を行いたい場合は、変数宣言時にmutをつける必要があります(ここでもRustのデフォルト不変のルールが適用されます)。


2. 列挙型(Enum):RustのEnumはただの「列挙」じゃない!

次に紹介するのが列挙型(Enum)です。
「Enumなんて他の言語でも使ったことあるよ(定数のリストでしょ?)」と思った方、ちょっと待ってください!

RustのEnumは、C言語やJavaなどのEnumとは一味違います。
最大の特徴は、Enumの各バリアント(選択肢)の中に、直接データを持たせることができる点です。

基本的なEnum

まずは普通のEnumを見てみましょう。

enum IpAddrKind {
    V4,
    V6,
}

これは「IPアドレスはV4かV6のどちらかである」という状態を表します。

データを持つEnum(Rustの真骨頂)

ここからが本番です。Rustでは、各種類ごとに具体的なデータを埋め込むことができます。

enum Message {
    Quit,                       // データなし
    Move { x: i32, y: i32 },    // ス構造体のように名前付きフィールドを持つ
    Write(String),              // Stringを持つ
    ChangeColor(i32, i32, i32), // 3つのi32を持つ
}

このように、Messageという一つの型の中に、全く異なる形式のデータを共存させることができます。
「終了シグナル」も「座標移動」も「テキストメッセージ」も、全て同じMessage型として扱えるため、関数の引数などで非常に取り回しが良くなります。


3. match式:パターンマッチで安全な分岐を作る

構造体やEnumを作っても、それを使えなければ意味がありません。
Rustには、Enumの可能性を最大限に引き出す「match式」という制御構文があります。

ifやswitchに似ていますが、決定的な違いは「全てのパターンを網羅しないとコンパイルエラーになる」という点です。これにより、「想定外のパターンが来てバグった!」という事態を防げます。

match式の基本

先ほどのMessage Enumを使って、ロジックを組んでみましょう。

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("プログラムを終了します");
        }
        Message::Move { x, y } => {
            println!("座標({}, {})へ移動しました", x, y);
        }
        Message::Write(text) => {
            println!("メッセージ: {}", text);
        }
        Message::ChangeColor(r, g, b) => {
            println!("色をR:{} G:{} B:{}に変更", r, g, b);
        }
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    process_message(msg);
}

「漏れ」があると怒られる優しさ

もし上記のコードで、Message::Quitの処理を書き忘れたとしましょう。
Rustコンパイラは即座にエラーを出します。

error[E0004]: non-exhaustive patterns: ‘Quit’ not covered(パターンが網羅されていません。’Quit’がカバーされていません)

これがRustの安全性です。「処理忘れ」というバグを、実行する前に完全になくすことができるのです。


4. 実践:RPGのキャラクターを作ってみよう

ここまでの知識(構造体、Enum、match)を組み合わせて、簡単なRPGのようなコードを書いてみましょう。

  • 構造体:キャラクターのステータス
  • Enum:キャラクターの職業(職業ごとに固有のスキルデータを持つ)
  • match:職業ごとの攻撃アクション
// 職業ごとの詳細データを持つEnum
enum Job {
    Warrior,                // 戦士(データなし)
    Mage { mp: i32 },       // 魔法使い(MPを持つ)
    Archer(i32),            // 弓使い(矢の本数を持つ)
}

// キャラクターを表す構造体
struct Character {
    name: String,
    hp: i32,
    job: Job, // ここでEnumを使用!
}

fn main() {
    // 魔法使いのキャラクターを作成
    let player = Character {
        name: String::from("Rust使い"),
        hp: 100,
        job: Job::Mage { mp: 50 },
    };

    attack(player);
}

// 攻撃処理を行う関数
fn attack(char: Character) {
    print!("{}の攻撃!: ", char.name);

    // 職業によって処理を分岐
    match char.job {
        Job::Warrior => println!("剣で強力な一撃!"),
        Job::Mage { mp } => {
            if mp > 10 {
                println!("ファイアボール発射! (残りMP: {})", mp - 10);
            } else {
                println!("MPが足りない!杖で殴った!");
            }
        },
        Job::Archer(arrows) => println!("矢を放った! (残り矢: {})", arrows - 1),
    }
}

構造体の中にEnumを入れることで、「キャラクター」という大きな枠組みの中に、「職業ごとの個別の事情」まできれいに整理することができました。


まとめ:データ構造を制する者はRustを制す

今回は、Rustでのデータ整理の基本となる3つの要素を学びました。

  • 構造体 (struct):関連データをパッケージ化する。
  • 列挙型 (enum):データの種類を定義し、それぞれにデータを持たせる。
  • match式:Enumの全パターンを漏れなく安全に処理する。

特にEnumとmatchの組み合わせは、Rustプログラミングにおいて「最強のコンビ」と言われます。
エラー処理(Result/Option)など、Rustの核心部分でもこの仕組みが使われています。

次回は、いよいよ「モジュールシステム」について触れていきたいと思います。プログラムが大きくなってきたときに、ファイルを分割して管理する方法を学びましょう!

にいやん

出身 : 関西 居住区 : 関西 職業 : 組み込み機器エンジニア (エンジニア歴13年) 年齢 : 38歳(2022年11月現在) 最近 業務の効率化で噂もありPython言語に興味を持ち勉強しています。 そこで学んだことを記事にして皆さんとシェアさせていただければと思いブログをはじめました!! 興味ある記事があれば皆さん見ていってください!! にほんブログ村