[Rustのはじめ方] Part17: 列挙型(enum)とmatchの使い方

列挙型(enum)とmatchの使い方 🧩

列挙型(enum)って何? 🤔

プログラミングをしていると、「この値は、いくつかの決まった選択肢のうちのどれか一つ」という状況によく出会います。例えば、「信号の色(赤・黄・青)」や「処理の状態(成功・失敗・処理中)」などです。

Rustでは、このような「複数の可能性の中から一つを選ぶ」型を定義するために列挙型(enum)を使います。enum は “enumeration” の略で、「列挙する」という意味があります。

列挙型を使うと、コードがより安全で分かりやすくなります。なぜなら、想定外の値が入ることを防ぎ、その型が取りうる値を明確に示せるからです。

基本的な列挙型の定義

列挙型は enum キーワードを使って定義します。取りうる値(ヴァリアント (variant) と呼びます)を {} の中に列挙します。


// 信号の色を表す列挙型
enum SignalColor {
    Red,
    Yellow,
    Blue,
}

fn main() {
    let red_signal = SignalColor::Red;
    let yellow_signal = SignalColor::Yellow;
    let blue_signal = SignalColor::Blue;
}
      

この例では、SignalColor という名前の列挙型を定義し、そのヴァリアントとして Red, Yellow, Blue を定義しています。ヴァリアントには 列挙型名::ヴァリアント名 の形式でアクセスします。

値を持つ列挙型

Rustの列挙型は、ヴァリアントにデータを持たせることもできます。これは非常に強力な機能です 💪。データを持つ方法はいくつかあります。

タプル風ヴァリアント

ヴァリアントにタプルのようにデータを持たせることができます。


enum Message {
    Quit, // データを持たないヴァリアント
    Write(String), // String型のデータを持つヴァリアント
    Move { x: i32, y: i32 }, // 匿名の構造体を持つヴァリアント
    ChangeColor(u8, u8, u8), // 3つのu8型のデータを持つヴァリアント
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Write(String::from("こんにちは"));
    let msg3 = Message::Move { x: 10, y: 20 };
    let msg4 = Message::ChangeColor(255, 0, 0); // 赤色
}
      

このように、ヴァリアントごとに異なる型や数のデータを持たせることができます。

match式によるパターンマッチング ✨

列挙型の値に応じて処理を分岐させたい場合、Rustでは match 式を使います。match 式は、値を取りうるパターンと比較し、一致したパターンの処理を実行します。

match 式は非常に強力で、Rustの多くの場面で使われます。特に列挙型との相性は抜群です!


enum SignalColor {
    Red,
    Yellow,
    Blue,
}

fn action(color: SignalColor) {
    match color {
        SignalColor::Red => {
            println!("止まれ! 🛑");
        }
        SignalColor::Yellow => {
            println!("注意して進め! ⚠️");
        }
        SignalColor::Blue => {
            println!("進め! ✅");
        }
    }
}

fn main() {
    action(SignalColor::Red);
    action(SignalColor::Blue);
}
      

match 式では、match キーワードの後に比較したい値を書き、{} ブロック内に パターン => 実行する式, の形式で「アーム」を記述します。値がいずれかのアームのパターンに一致すると、対応する式が実行されます。

値を持つ列挙型とmatch

データを持つ列挙型を match で扱う場合、パターン内でそのデータを取り出して利用できます。


enum Message {
    Quit,
    Write(String),
    Move { x: i32, y: i32 },
    ChangeColor(u8, u8, u8),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("終了メッセージを受け取りました。");
        }
        Message::Write(text) => { // text変数にString型のデータが束縛される
            println!("書き込みメッセージ: {}", text);
        }
        Message::Move { x, y } => { // xとy変数にそれぞれの値が束縛される
            println!("移動メッセージ: x={}, y={}", x, y);
        }
        Message::ChangeColor(r, g, b) => { // r, g, b変数にそれぞれのu8値が束縛される
            println!("色変更メッセージ: R={}, G={}, B={}", r, g, b);
        }
    }
}

fn main() {
    process_message(Message::Write(String::from("Hello, Rust!")));
    process_message(Message::Move { x: 5, y: -2 });
}
      

パターン内で変数名を指定することで、ヴァリアントが持つデータをその変数に束縛(代入のようなもの)して、=> の右側の式で利用できます。

Option型とmatch

Rustには「値が存在しないかもしれない」状況を表すための標準的な列挙型 Option<T> があります。これは非常によく使われます。


enum Option<T> {
    Some(T), // 値が存在する場合。Tはジェネリック型
    None,    // 値が存在しない場合
}
      

Option<T> 型の値は、match を使って安全に中身を取り出すことができます。


fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None, // 値がなければNoneを返す
        Some(i) => Some(i + 1), // 値があればそれに1を足してSomeで包んで返す
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

    println!("five: {:?}", five); // Some(5)
    println!("six: {:?}", six);   // Some(6)
    println!("none: {:?}", none); // None
}
      

match を使うことで、「値がない (None)」場合を忘れずに処理することを強制できるため、nullポインターに起因するバグを防ぐのに役立ちます。🌟

match式の網羅性(Exhaustiveness)

Rustの match 式は網羅的 (exhaustive) である必要があります。つまり、match で扱う値のすべての可能性をアームでカバーしなければなりません。もしパターンが足りないと、コンパイルエラーになります。🙅‍♀️


fn main() {
    let some_option: Option<i32> = Some(10);

    match some_option { // これはコンパイルエラーになる! None の場合のパターンがないため
        Some(i) => println!("値は {} です", i),
        // None の場合の処理が書かれていない!
    }
}
      
コンパイルエラー: non-exhaustive patterns: `None` not covered

すべてのヴァリアントを列挙するのが大変な場合や、特定のパターン以外は同じ処理をしたい場合は、特殊なパターン _ (アンダースコア)を使うことができます。_ は「それ以外のすべての値」にマッチするワイルドカードとして機能します。


fn main() {
    let dice_roll = 9;

    match dice_roll {
        3 => println!("3が出ました!🥉"),
        7 => println!("7が出ました! 🎉"),
        other => println!("{} が出ました。", other), // 3と7以外の場合、その値がotherに束縛される
    }

    match dice_roll {
        1 => println!("1が出ました!"),
        _ => println!("1以外の何かが出ました。"), // 1以外の場合はすべてこのアームが実行される
                                              // _ は値を束縛しない
    }
}
      

_ を使うことで、明示的に処理しない値を無視しつつ、網羅性を満たすことができます。

if let 構文

match 式は強力ですが、特定の1つのパターンにだけマッチした場合に処理を行い、それ以外は無視したい、というケースもよくあります。そのような場合、match 式は少し冗長になることがあります。

Rustには、このようなケースをより簡潔に書くための if let 構文が用意されています。


fn main() {
    let config_max = Some(3u8);

    // match を使う場合
    match config_max {
        Some(max) => println!("最大値は {} に設定されています", max),
        None => (), // None の場合は何もしない
    }

    // if let を使う場合 (より簡潔!)
    if let Some(max) = config_max {
        println!("最大値は {} に設定されています (if let)", max);
    } else {
        // 必要であれば else ブロックで None の場合の処理も書ける
        println!("最大値は設定されていません (if let + else)");
    }

    let config_none: Option<u8> = None;
    if let Some(max) = config_none {
        println!("最大値は {} です", max); // このブロックは実行されない
    } else {
        println!("最大値は設定されていません (if let + else, None の場合)");
    }
}
      

if let パターン = 値 { ... } のように書きます。パターン にマッチした場合にのみ {} ブロック内のコードが実行されます。match の網羅性チェックは失われますが、特定のパターンだけを簡潔に扱いたい場合に便利です。👍

まとめ

今回はRustの列挙型(enum)と、それと密接に関連する match 式、そして if let 構文について学びました。

  • 列挙型 (enum): 複数の可能性の中から一つを選ぶ型を定義する。ヴァリアントにデータを持たせることもできる。
  • match式: 値をパターンと比較し、一致したアームの処理を実行する。網羅的である必要がある。
  • _ ワイルドカード: match で「それ以外」のパターンを表す。
  • Option<T>: 値が存在しない可能性を表す標準的な列挙型。match で安全に扱える。
  • if let: 特定のパターンにのみマッチさせたい場合に match より簡潔に書ける構文。

列挙型とパターンマッチングは、Rustの安全性と表現力を支える重要な機能です。ぜひ使いこなせるようになりましょう! 😊

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です