404 Not Found

404 Not Found


nginx

Rustの列挙型とOption:パターンマッチングに最適な組み合わせ

列挙型(Enum)はRustで最も強力な型の一つであり、値が複数の可能なバリエーションのうちのいずれか一つとなることを可能にし、各バリエーションは異なるデータを保持することができます。

構造体が「AND」型(すべてのフィールドを同時に持つ)であるのに対し、列挙型は「OR」型(これかあれかのいずれか)です。


1. 学習内容


2. 概念図

100%
flowchart LR
    subgraph "enum Option<T>"
        S["Some(T)<br>Not null"]
        N["None<br>No value"]
    end
    subgraph "Use Cases"
        V["Calculation Results"] -->|"Success"| S
        F["Calculation Results"] -->|"Failure/None"| N
    end
    S --> MATCH["match Processing"]
    N --> MATCH
    MATCH -->|"Some(v) => Usage Value v"| OK["✅ Safety"]
    MATCH -->|"None => Handling Null Values"| SAFE["✅ No crashes"]

3. 注文システムの物語

(1) 課題:注文状況を数字で表現すること

トムはEコマースシステムを開発しています。注文には4つのステータスがあります:

TEXT
0 = Payable, 1 = Paid, 2 = Shipped, 3 = Completed
RUST
let order_status = 0;  // Payable
// But what if someone were to write order_status = 99?
// Or confuse the status with the quantity: let order_status = product_count;

C言語では、状態を整数で表現するのが一般的ですが、これは型安全ではありません。どのような整数でも「状態」に代入することができ、コンパイラがそれをチェックしてくれることはありません。さらに、状態同士の間に何の関係もないため、エラーが発生しやすくなります。

(2) Rustの列挙型に関する解決策

RUST
enum OrderStatus {
    Pending,       // Payable
    Paid,          // Paid
    Shipped,       // Shipped -- you can bring your shipping tracking number
    Delivered,     // Completed -- delivery time (in-person)
}

fn main() {
    let status = OrderStatus::Paid;

    match status {
        OrderStatus::Pending => println!("Please complete the payment"),
        OrderStatus::Paid => println!("Paid, awaiting shipment"),
        OrderStatus::Shipped => println!("Shipped, on the way"),
        OrderStatus::Delivered => println!("Delivered, thank you for your purchase"),
    }

    // The compiler ensures that: you won't forget to handle any state!
    // If a new status is added Cancelled, forgot to update match, the compiler will report an error
}

列挙型は、考えられるすべての「選択肢」を単一の型にまとめます。列挙型で match を使用することで、網羅的なチェックが保証されます。つまり、すべての状態が処理され、何も見落とされることはありません。


4. 列挙型

(1) 列挙型のバリアントはデータを保持できる

RUST
enum Message {
    Quit,                       // No data available
    Move { x: i32, y: i32 },   // Anonymous Structures
    Write(String),              // Single value
    ChangeColor(i32, i32, i32), // Tuple
}
列挙型 格納されるデータ 用途
Quit なし 単なる目印
Move { x, y } 匿名構造体 Message::Move { x: 10, y: 20 }
Write(String) タプルの構造 Message::Write("hello".to_string())
ChangeColor(i,i,i) タプルの構造 Message::ChangeColor(255, 0, 0)

(2) 一般的なオプション手法のクイックリファレンス

メソッド 戻り値の型 説明 None の場合の挙動
unwrap() T 値の抽出 パニック
expect(msg) T 値の取得(カスタムメッセージ) panic + msg
unwrap_or(default) T 値またはデフォルト値を取得 デフォルト値を返す
map(f) Option<U> 「Some」の値を変換する 「None」のままにする
and_then(f) Option<U> チェーンオプションの操作 なし
filter(f) Option<T> 条件フィルター 条件を満たさない場合はなし
is_some() bool 値がある false
is_none() bool なし true
ok_or(err) Result<T, E> 結果に変換 Err(err)

(3) オプションとヌルの比較

ディメンション Option<T> null (その他の言語)
型安全性 コンパイラがnull値の処理を強制する どの参照もnullになり得る
ヌルチェック match/メソッドチェーンによる自動オーバーライド 手動での if チェックが必要
確認し忘れた コンパイルエラー 実行時のNullPointerException
連鎖演算 map/and_then など ネストした if 文またはオプションチェーンが必要

(4) 列挙型のメモリ配置

列挙型が使用するメモリ = 最大のバリアントが使用するメモリ + タグ用の1バイト。コンパイラは、このタグを使用して、現在どのバリアントが格納されているかを判断します。

TEXT
graph TB
    subgraph "Message Enumerations in Memory"
        TAG[tag: 1 Byte] -->|Indicates which variant is currently selected| LABEL
        DATA[Data on the Largest Variant: 8 Byte] --> LABEL[In total 9 Byte]
    end

5. 列挙の例

(1) ▶ サンプル:列挙型のバリアントは異なる型のデータを保持する (難易度 ⭐⭐)

RUST
// ============================================
// Enumeration variants carry data -- Message Type
// ============================================

#[derive(Debug)]
enum Message {
    Quit,                              // No data available
    Move { x: i32, y: i32 },          // Anonymous Structures
    Write(String),                     // Tuple Structure
    ChangeColor(i32, i32, i32),       // Tuple Structure
}

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("Quit: Exit"),
            Message::Move { x, y } => println!("Move: Move to ({}, {})", x, y),
            Message::Write(text) => println!("Write: Message Content: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!("ChangeColor: Color RGB({}, {}, {})", r, g, b);
            }
        }
    }
}

fn main() {
    let messages = vec![
        Message::Write(String::from("Hello")),
        Message::Move { x: 10, y: 20 },
        Message::ChangeColor(255, 0, 0),
        Message::Quit,
    ];

    for msg in &messages {
        msg.call();
    }
}

出力:

TEXT
Write: Message Content: Hello
Move: Move to (10, 20)
ChangeColor: Color RGB(255, 0, 0)
Quit: Exit

列挙型の各バリアントは、異なる数のデータや異なる型のデータを保持することができます。Quitにはデータがなく、Moveには匿名構造体が、WriteにはStringが、ChangeColorには3つのi32が格納されています。この点において、列挙型は構造体よりも柔軟性が高いと言えます。


(2) ▶ サンプル:Option<T>—Rust におけるヌル値の扱い(難易度 ⭐⭐)

RUST
// ============================================
// Option<T> Enumeration: Not null (Some) or null (None)
// ============================================

// Option Definition (in the standard library)
// enum Option<T> {
//     Some(T),
//     None,
// }

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None  // Back "no value" -- instead of crashing or returning NaN
    } else {
        Some(a / b)  // Back "not null"
    }
}

fn main() {
    let result1 = divide(10.0, 2.0);
    let result2 = divide(10.0, 0.0);

    // Use match to process Option
    match result1 {
        Some(value) => println!("10 / 2 = {}", value),
        None => println!("The divisor is 0"),
    }

    match result2 {
        Some(value) => println!("10 / 0 = {}", value),
        None => println!("The divisor is 0"),
    }

    // Simplify: unwrap or expect (risky, but convenient)
    // println!("{}", result2.unwrap());  // ❌ unwrap on None will panic!
    println!("result1 the value of: {}", result1.unwrap());  // ✅ unwrap on Some is safe
}

出力:

TEXT
10 / 2 = 5
The divisor is 0
result1 the value of: 5

Option<T> は、Rust において「null である可能性がある」値を扱うための標準的な方法です。null/nil/None によって実行時にクラッシュを引き起こす他の言語とは異なり、Rust の Option<T> では、match または unwrap を使用して、「null ではない」場合と「null」の場合の両方を明示的に処理する必要があります。


(3) ▶ サンプル:Option の一般的な解法(難易度 ⭐⭐)

RUST
// ============================================
// Option Practical Methods for: map, unwrap_or, expect
// ============================================

fn main() {
    let some_value: Option<i32> = Some(10);
    let none_value: Option<i32> = None;

    // map: If there is a value, convert it; no value, maintain None
    let doubled_some = some_value.map(|x| x * 2);
    let doubled_none = none_value.map(|x| x * 2);
    println!("map after: {:?}, {:?}", doubled_some, doubled_none);

    // unwrap_or: Returns a value, if no value is provided, the default is used.
    println!("unwrap_or: {}, {}", 
        some_value.unwrap_or(0),   // 10
        none_value.unwrap_or(0),   // 0
    );

    // expect: Returns a value, if no value, panic and display a custom message
    println!("expect: {}", some_value.expect("There should be a value"));
    // println!("expect: {}", none_value.expect("OH NO! No value!"));  // ❌ panic

    // is_some / is_none: Check if a value exists
    println!("is_some: {}, is_none: {}", 
        some_value.is_some(), 
        none_value.is_none(),
    );

    // Example of a Chain Call
    let result = Some(5)
        .map(|x| x + 3)
        .map(|x| x * 2)
        .unwrap_or(0);
    println!("Results of a chained call: {}", result);  // 16
}

出力:

TEXT
map after: Some(20), None
unwrap_or: 10, 0
expect: 10
is_some: true, is_none: true
Results of a chained call: 16

Option<T> は、毎回 match を記述する必要なく、関数型スタイルでヌル値となる可能性のある値に対して操作を連鎖させることができる一連のメソッドを提供します。map は値の変換を行い、unwrap_or はデフォルト値を提供し、expect はデバッグ時により明確なエラーメッセージを表示します。


(4) ▶ サンプル:列挙の網羅性と match (難易度 ⭐⭐⭐)

RUST
// ============================================
// Enumerate proprietary match Exhaustive Check Demonstration
// ============================================

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky Penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
        // If I don't write Quarter, the compiler will report an error: non-exhaustive patterns
    }
}

// Enumeration with Data
#[derive(Debug)]
enum IpAddr {
    V4(u8, u8, u8, u8),  // 4 bytes
    V6(String),           // Complete IPv6 Address
}

fn main() {
    let coin = Coin::Quarter;
    println!("Value of Coins: {} cents", value_in_cents(coin));

    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    match home {
        IpAddr::V4(a, b, c, d) => {
            println!("IPv4: {}.{}.{}.{}", a, b, c, d);
        }
        IpAddr::V6(addr) => {
            println!("IPv6: {}", addr);
        }
    }
}

出力:

TEXT
Value of Coins: 25 cents
IPv4: 127.0.0.1

列挙型のすべてのバリアントは、match ステートメントで処理されなければなりません。これは「網羅的チェック」として知られています。バリアントの処理を忘れると、コンパイラはエラーを報告します。これはC言語のswitchよりもはるかに安全です(C言語では、switch文内でcaseを忘れた場合、単に黙ってスキップされてしまいます)。


(5) ▶ サンプル:総合演習—ユーザー入力パーサー(難易度 ⭐⭐⭐)

RUST
// ============================================
// Comprehensive Example: Enumeration + Option + match Real-World Experience
// ============================================

#[derive(Debug)]
enum Command {
    Help,
    Greet(String),
    Calc(f64, char, f64),
    Quit,
}

fn parse_command(input: &str) -> Option<Command> {
    let parts: Vec<&str> = input.trim().split_whitespace().collect();
    if parts.is_empty() {
        return None;
    }
    match parts[0] {
        "help" | "h" => Some(Command::Help),
        "quit" | "q" => Some(Command::Quit),
        "greet" if parts.len() > 1 => {
            Some(Command::Greet(parts[1..].join(" ")))
        }
        "calc" if parts.len() == 4 => {
            let a = parts[1].parse::<f64>().ok()?;
            let op = parts[2].chars().next()?;
            let b = parts[3].parse::<f64>().ok()?;
            Some(Command::Calc(a, op, b))
        }
        _ => None,
    }
}

fn execute(cmd: Command) -> bool {
    match cmd {
        Command::Help => {
            println!("Command: help | greet `<name>` | calc `<a>` `<op>` `<b>` | quit");
            true
        }
        Command::Greet(name) => {
            println!("Hello, {}!", name);
            true
        }
        Command::Calc(a, op, b) => {
            let result = match op {
                '+' => Some(a + b),
                '-' => Some(a - b),
                '*' => Some(a * b),
                '/' if b != 0.0 => Some(a / b),
                '/' => { println!("Error: Divisor is zero"); None }
                _ => { println!("Unknown Operator: {}", op); None }
            };
            result.map(|r| println!("{} {} {} = {:.2}", a, op, b, r));
            true
        }
        Command::Quit => {
            println!("Goodbye!");
            false
        }
    }
}

fn main() {
    let inputs = [
        "help",
        "greet Alice",
        "calc 10 + 5",
        "calc 20 / 4",
        "calc 1 / 0",
        "unknown",
        "quit",
    ];

    println!("=== Command Parser ===");
    for input in inputs {
        println!("\n> {}", input);
        match parse_command(input) {
            Some(cmd) => {
                if !execute(cmd) { break; }
            }
            None => println!("Unrecognized command"),
        }
    }
}

出力:

TEXT
=== Command Parser ===

> help
Command: help | greet `<name>` | calc `<a>` `<op>` `<b>` | quit

> greet Alice
Hello, Alice!

> calc 10 + 5
10 + 5 = 15.00

> calc 20 / 4
20 / 4 = 5.00

> calc 1 / 0
Error: Divisor is zero

> unknown
Unrecognized command

> quit
Goodbye!

この例では、異なるデータを保持する列挙型のバリアント、Option 連鎖演算 (ok()?, .ok()?)、match 網羅的なチェック、および parse_command return Option<Command> パターンを組み合わせています。Rust において、「複数の結果の可能性」を扱う際の標準的なパラダイムは、enum + Option + match です。


❓ よくある質問

Q 列挙型と構造体の違いは何ですか?
A 構造体は「両方とも」(and型)であるのに対し、列挙型は「どちらか一方」(or型)です。
Q なぜRustはnullではなくOptionを使うのですか?
A Optionは型安全です。コンパイラが、値が「nullである可能性がある」ケースへの対処を強制するからです。
Q someunwrap の違いは何ですか?
A some は「値があるかもしれない」という意味で、unwrap は「確実に値がある。そうでなければクラッシュする」という意味です。
Q 列挙型にはどのくらいのデータを格納できますか?
A 列挙型が占める領域 = 最大のバリアントのサイズ + 1バイトのタグ。
Q enumのバリアント内のデータにはどのような名前を付けますか?
A 匿名構造体の構文を使用します。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐:月曜日から日曜日までを含む列挙型 Weekday を定義してください。Weekday を入力として受け取り、それが平日か週末かを(match を使用して)返す関数を記述してください。
  2. 難易度 ⭐⭐: x >= 0 の場合は fn safe_sqrt(x: f64) -> Option<f64> を返し、それ以外の場合は None を返す関数 fn safe_sqrt(x: f64) -> Option<f64> を記述してください。main 内で、正の数と負の数の両方をテストしてください。
  3. 難易度 ⭐⭐⭐: Temperature という列挙型を定義し、その中に Celsius(f64)Fahrenheit(f64) の 2 つのバリエーションを含めます。fn to_celsius(&self) -> f64 および fn to_fahrenheit(&self) -> f64 メソッドを実装してください。main では、2 つの温度値を作成し、それらを相互に変換してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%