Rustの列挙型(enum)とは?基本から実践的な使い方まで解説!

2025年4月21日月曜日

Rust

Rustの列挙型(enum)、プログラミング学習中によく耳にする言葉かもしれませんね。

「他の言語にもある、あのenumでしょ?」と思ったそこのあなた、ちょっと待ってください!Rustのenumは、ひと味もふた味も違う、とってもパワフルなやつなんです。

簡単に言うと、列挙型は関連するいくつかの値をひとまとめにして、新しい型を定義する機能のこと。

例えば、「信号機の色」という型を作って、「赤」「黄」「青」という値だけを入れられるようにする、みたいな感じです。

でもRustのすごいところは、それぞれの値(バリアントと呼びます)に、さらに情報を持たせることができる点!これがもう、本当に便利なんですよ。

この記事では、Rustの列挙型が…

  • そもそも何なのか?
  • どうやって書くのか?
  • どうやって使うのか(特にmatch式との連携!)
  • Rustで超よく見るOption型やResult型って何?

といった疑問に、初心者の方にも「なるほど!」と思っていただけるように、サンプルコード満載で解説していきます。

Rustの列挙型の基本的な書き方(定義方法)

まずは基本の「キ」、列挙型の定義方法から見てみましょう。

Rustでは、enumというキーワードを使って定義します。型名やバリアント名は、最初の文字を大文字にするアッパーキャメルケース(UpperCamelCase)で書くのが習わしです。

一番シンプルな列挙型はこんな感じ。

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

// 使い方(変数に代入してみる)
let red_signal = Signal::Red;
let yellow_signal = Signal::Yellow;

enum Signalで「Signal」という名前の新しい型を定義しています。

その中に、Red, Yellow, Blue という3つの可能な値をバリアントとして定義しました。
使うときは、型名::バリアント名(例:Signal::Red)という形で指定します。シンプルですよね!

各バリアントにデータを持たせる

ここからがRustの列挙型の真骨頂!各バリアントに追加情報、つまり「データ」を持たせることができるんです。

例えば、メッセージの種類によって、持つ情報が違う場合なんかにすごく役立ちます。

データを持たせるには、バリアント名の後ろに(){}をつけます。

enum Message {
    Quit, // データなし
    Move { x: i32, y: i32 }, // 構造体のように名前付きデータ
    Write(String), // タプルのようにデータ(文字列)
    ChangeColor(i32, i32, i32), // タプルのように複数のデータ(RGB値)
}

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

こんな風に、Quitは何もしない、Moveはxとy座標を持つ、Writeは文字列を持つ、ChangeColorはRGB値を持つ、といった具合に、バリアントごとに異なるデータ構造を定義できるのが、めちゃくちゃ柔軟でしょ?

Rustの列挙型の使い方 - `match`式でパターンマッチング

列挙型を定義したのはいいけれど、どうやって使うの?という疑問にお答えしましょう。
ここで登場するのが、Rustの強力な制御フロー構文、match式です!

match式は、ある値が、指定したパターンのどれに一致するかを調べて、一致した場合に対応する処理を実行する仕組みです。

先ほどのSignal列挙型とmatch式を組み合わせてみましょう。

enum Signal {
    Red,
    Yellow,
    Blue,
}

fn check_signal(signal: Signal) {
    match signal {
        Signal::Red => {
            println!("止まれ!");
        }
        Signal::Yellow => {
            println!("注意して進め");
        }
        Signal::Blue => {
            println!("進め!");
        }
    }
}

fn main() {
    let current_signal = Signal::Yellow;
    check_signal(current_signal); // 出力: 注意して進め
}

match signal { ... } の部分がmatch式です。
signal変数の値が、Signal::RedSignal::YellowSignal::Blueのどれに一致するかを順番にチェックします。

そして、一致したパターンの=>の右側の処理({}で囲まれたコードブロック)が実行される、という流れです。

ここで大事なのは、match式は網羅的(exhaustive)でなければならない点。
つまり、考えられる全てのバリアントに対する処理を書かないと、コンパイルエラーになります。

「もし青信号の場合の処理を書き忘れたら?」なんて心配は無用!コンパイラが「おい、Blueの場合の処理が抜けてるぞ!」と教えてくれるので、うっかりミスを防げるんです。安全ですね!

もし、「特定のいくつか以外は全部同じ処理でいい」という場合は、_(アンダースコア)を使って、それ以外の全てのパターンを表すことができます。

// ... Signal enum definition ...

fn check_signal_short(signal: Signal) {
    match signal {
        Signal::Red => println!("Stop!"),
        // Red以外はまとめて処理
        _ => println!("Go or Caution!"),
    }
}

データを持つ列挙型と`match`式

データを持つ列挙型も、もちろんmatch式で扱えます。
しかも、マッチした際にバリアントが持つデータを取り出して、その後の処理で使うことも可能なんです。

先ほどのMessage列挙型で試してみましょう。

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

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("終了メッセージを受け取りました。");
        }
        // Moveバリアントにマッチし、中のxとyを取り出す
        Message::Move { x, y } => {
            println!("座標 ({}, {}) へ移動します。", x, y);
        }
        // Writeバリアントにマッチし、中の文字列を取り出す
        Message::Write(text) => {
            println!("メッセージ: {}", text);
        }
        // ChangeColorバリアントにマッチし、RGB値を取り出す
        Message::ChangeColor(r, g, b) => {
            println!("色を RGB({}, {}, {}) に変更します。", r, g, b);
        }
    }
}

fn main() {
    let msg1 = Message::Move { x: 5, y: 15 };
    let msg2 = Message::Write(String::from("Rust楽しい!"));

    process_message(msg1); // 出力: 座標 (5, 15) へ移動します。
    process_message(msg2); // 出力: メッセージ: Rust楽しい!
}

Message::Move { x, y } => ... の部分では、Moveバリアントにマッチした場合、その中のxyフィールドの値が、そのままxyという名前の変数に束縛(代入みたいなもの)されます。

Message::Write(text) => ... でも同様に、Writeバリアントが持つString型のデータがtextという変数に入ります。

このように、`match`式を使うと、バリアントの種類に応じた処理をしつつ、中のデータもスマートに取り出せるわけです。これは本当に便利!

`if let` で特定のバリアントだけを扱う

match式は全てのパターンを書く必要があって安全ですが、「このバリアントの場合だけ、何か特別な処理をしたいな。他はどうでもいいんだけど」という場面も、結構ありますよね。
そんな時に便利なのがif let構文です。

例えば、Message型の値がWriteバリアントだったら、その中のメッセージを表示したい、それ以外は何もしない、という場合はこう書けます。

// ... Message enum definition ...

fn main() {
    let msg = Message::Write(String::from("if let のテスト"));
    // let msg = Message::Quit; // こちらを有効にするとifブロックは実行されない

    // msgがMessage::Writeパターンにマッチする場合のみ、中の値をtextに束縛してブロック内を実行
    if let Message::Write(text) = msg {
        println!("if let でメッセージ発見: {}", text);
    } else {
        // マッチしなかった場合の処理(任意)
        println!("Writeメッセージではありませんでした。");
    }
}

if let パターン = 値 { ... } という形で書きます。

パターンにマッチすれば、{}の中の処理が実行されます。match式と違って、特定のパターンにだけ注目できるので、コードがスッキリする場合があります。

ちなみに、ループで使うwhile letなんていうのもありますよ。

Rustの列挙型にメソッドを実装する (`impl`)

Rustでは、構造体(struct)だけでなく、列挙型(enum)にもメソッドを実装できます。
implキーワードを使う点は構造体と同じです。

enum Signal {
    Red,
    Yellow,
    Blue,
}

// Signal列挙型にメソッドを実装
impl Signal {
    // 現在の信号の色を表す文字列を返すメソッド
    fn description(&self) -> &str {
        match self {
            Signal::Red => "赤信号",
            Signal::Yellow => "黄信号",
            Signal::Blue => "青信号",
        }
    }
}

fn main() {
    let sig = Signal::Yellow;
    // メソッド呼び出し
    println!("現在の信号: {}", sig.description()); // 出力: 現在の信号: 黄信号
}

impl Signal { ... } のブロック内に、fn description(&self) ... のようにメソッドを定義します。
&self は、メソッドが呼び出された列挙型の値自身(この例ではsig)を受け取るための引数です。

こうすることで、列挙型に関連する処理を、その列挙型自身にまとめて定義できるので、コードの整理がしやすくなりますね。

Rustでよく使われる標準ライブラリの列挙型 - `Option<T>` と `Result<T, E>`

Rustを書いていく上で、避けては通れない、そして非常に重要な標準ライブラリの列挙型が2つあります。

それがOptionResultです。
これらも実は列挙型なんですよ!

  • Option
    • 値が「あるかもしれないし、ないかもしれない」状況を表します。
  • Result
    • 処理が「成功したかもしれないし、失敗したかもしれない」状況を表します。

これらの列挙型とmatch式を組み合わせることで、Rustはプログラムの安全性(特にnull参照のようなエラーの防止や、エラー処理の強制)を高めています。

`Option<T>` - 値が存在しないかもしれない場合

他の言語でよくある「null」や「nil」といった「値がない」状態を表す値は、うっかりアクセスするとエラー(ぬるぽ!)の原因になりがちです。

Rustでは、値がない可能性を明示的に扱うためにOption型を使います。

Optionは2つのバリアントを持ちます。

  • Some(T): 値Tが存在する場合。
  • None: 値が存在しない場合。

例えば、配列から要素を探す関数を考えてみましょう。

見つかればその要素のインデックス(番号)を返したいですが、見つからない場合もありますよね?そんな時Option(usizeはインデックスを表す型)を使います。

fn find_element(haystack: &[i32], needle: i32) -> Option<usize> {
    for (index, &item) in haystack.iter().enumerate() {
        if item == needle {
            return Some(index); // 見つかった!インデックスをSomeで包んで返す
        }
    }
    None // 見つからなかった… Noneを返す
}

fn main() {
    let numbers = [10, 20, 30, 40, 50];

    match find_element(&numbers, 30) {
        Some(index) => println!("要素 30 はインデックス {} に見つかりました。", index),
        None => println!("要素 30 は見つかりませんでした。"),
    }
    // 出力: 要素 30 はインデックス 2 に見つかりました。

    match find_element(&numbers, 100) {
        Some(index) => println!("要素 100 はインデックス {} に見つかりました。", index),
        None => println!("要素 100 は見つかりませんでした。"),
    }
    // 出力: 要素 100 は見つかりませんでした。
}

find_element関数は、見つかればSome(インデックス)を、見つからなければNoneを返します。

呼び出し側はmatch式を使って、Someの場合(見つかった場合)とNoneの場合(見つからなかった場合)の両方の処理を必ず書く必要があります。これにより、「値がないかもしれない」状況への対処を忘れることがなくなります。

Optionのおかげで、Rustはnullポインタエラーから私たちを守ってくれるのです。ありがたいですね!

`Result<T, E>` - 処理が失敗する可能性がある場合

ファイルの読み書きやネットワーク通信など、プログラムの処理は失敗する可能性がつきものです。

Rustでは、このような成功/失敗を表すためにResult型を使います。

Resultも2つのバリアントを持ちます。

  • Ok(T): 処理が成功した場合。成功した値Tを持つ。
  • Err(E): 処理が失敗した場合。エラー情報Eを持つ。

例えば、文字列を数値に変換する処理を考えてみましょう。うまく変換できれば数値を返したいですが、文字列が数値として解釈できない場合はエラーになります。

fn parse_number(s: &str) -> Result<i32, String> {
    match s.parse::() {
        Ok(number) => Ok(number), // 変換成功!Okで包んで返す
        Err(parse_err) => Err(format!("数値への変換に失敗しました: {}", parse_err)), // 変換失敗!Errで包んで返す
    }
}

fn main() {
    let num_str1 = "123";
    let num_str2 = "abc";

    match parse_number(num_str1) {
        Ok(n) => println!("変換成功: {}", n),
        Err(e) => println!("エラー: {}", e),
    }
    // 出力: 変換成功: 123

    match parse_number(num_str2) {
        Ok(n) => println!("変換成功: {}", n),
        Err(e) => println!("エラー: {}", e),
    }
    // 出力: エラー: 数値への変換に失敗しました: invalid digit found in string
}

s.parse::() は、文字列si32型に変換しようとし、その結果をResult型で返します(ここではエラー型を単純なStringに変換しています)。

成功すればOk(数値)、失敗すればErr(エラー情報)が返ってくるので、呼び出し側はmatch式を使って、成功した場合と失敗した場合、両方の処理をきちんと書くことが強制されます。

Resultによって、エラーを見逃さず、適切に対処するコードを書くことが促されるのです。

Rustの列挙型を使う上での注意点

とても便利なRustの列挙型ですが、いくつか気をつけておくと良い点があります。

match式の網羅性

何度も言いますが、match式では全てのバリアントを網羅する必要があります。
コンパイラがチェックしてくれるので安心ですが、エラーが出たら「あ、パターンが足りないんだな」と思い出してください。_ワイルドカードも上手に使いましょう。

バリアント名のスコープ

列挙型のバリアントは、型名::バリアント名という形で使います(例: Signal::Red)。

もし、use Signal::*; のようにしてバリアント名を直接スコープに入れると、他の要素と名前が衝突する可能性があるので少し注意が必要です。

データを持つ場合のサイズ

列挙型が持つことができるデータの中で、一番サイズの大きいバリアントに合わせてメモリが確保されます。

あまり巨大なデータをバリアントに持たせすぎると、メモリ効率が悪くなる可能性もゼロではありません(が、最初のうちは気にしすぎなくても大丈夫!)。

とはいえ、Rustのコンパイラは非常に優秀なので、多くの間違いはコンパイル時に教えてくれます。エラーメッセージを恐れずに、しっかり読んで対処していくのが上達のコツですよ!

【まとめ】Rustの列挙型を理解して安全なコードを書こう!

お疲れ様でした!Rustの列挙型(enum)について、基本から応用まで駆け足で見てきましたがいかがでしたか?

この記事で学んだポイントを振り返ってみましょう。

  • 列挙型はenumキーワードで定義する。
  • バリアントにデータを持たせることができるのがRustのenumの強力な点。
  • match式で列挙型の値をパターンマッチングし、網羅的な処理を書ける。
  • if letで特定のパターンだけを簡潔に扱える。
  • implブロックで列挙型にメソッドを定義できる。
  • 標準ライブラリのOptionResultも列挙型で、Rustの安全性を支えている。

Rustの列挙型は、単なる値のリストではありません。データ構造と振る舞いを組み合わせ、状態を安全かつ表現力豊かにモデル化するための、まさにRustの「秘密兵器」とも言える機能です。

初めは少し戸惑うかもしれませんが、使いこなせるようになると、驚くほどコードが整理され、バグが減り、書くのが楽しくなるはずです!

【関連記事】
Rustとは?いま学ぶべき理由と特徴や始め方を詳しく解説

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ