Rustの列挙型とOption:パターンマッチングに最適な組み合わせ
列挙型(Enum)はRustで最も強力な型の一つであり、値が複数の可能なバリエーションのうちのいずれか一つとなることを可能にし、各バリエーションは異なるデータを保持することができます。
構造体が「AND」型(すべてのフィールドを同時に持つ)であるのに対し、列挙型は「OR」型(これかあれかのいずれか)です。
1. 学習内容
- 列挙型とバリアントの定義
- 列挙型のバリアントは、異なる型のデータを格納できる
implを使用して、列挙型にメソッドを追加するOption<T>列挙――Rustにおける「Null値」へのアプローチ- 列挙および
match文の網羅性の確認 Option<T>一般的なメソッド(unwrap、map、expect)
2. 概念図
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つのステータスがあります:
0 = Payable, 1 = Paid, 2 = Shipped, 3 = Completed
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の列挙型に関する解決策
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) 列挙型のバリアントはデータを保持できる
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バイト。コンパイラは、このタグを使用して、現在どのバリアントが格納されているかを判断します。
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) ▶ サンプル:列挙型のバリアントは異なる型のデータを保持する (難易度 ⭐⭐)
// ============================================
// 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();
}
}
出力:
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 におけるヌル値の扱い(難易度 ⭐⭐)
// ============================================
// 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
}
出力:
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 の一般的な解法(難易度 ⭐⭐)
// ============================================
// 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
}
出力:
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 (難易度 ⭐⭐⭐)
// ============================================
// 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);
}
}
}
出力:
Value of Coins: 25 cents
IPv4: 127.0.0.1
列挙型のすべてのバリアントは、
matchステートメントで処理されなければなりません。これは「網羅的チェック」として知られています。バリアントの処理を忘れると、コンパイラはエラーを報告します。これはC言語のswitchよりもはるかに安全です(C言語では、switch文内でcaseを忘れた場合、単に黙ってスキップされてしまいます)。
(5) ▶ サンプル:総合演習—ユーザー入力パーサー(難易度 ⭐⭐⭐)
// ============================================
// 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"),
}
}
}
出力:
=== 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_commandreturnOption<Command>パターンを組み合わせています。Rust において、「複数の結果の可能性」を扱う際の標準的なパラダイムは、enum + Option + match です。
❓ よくある質問
nullではなくOptionを使うのですか?Optionは型安全です。コンパイラが、値が「nullである可能性がある」ケースへの対処を強制するからです。some と unwrap の違いは何ですか?some は「値があるかもしれない」という意味で、unwrap は「確実に値がある。そうでなければクラッシュする」という意味です。📖 まとめ
- Enum は「共用型」であり、値はいくつかのバリエーションのいずれかになります
- 列挙型のバリアントは、データ(タプルまたは匿名構造体の形式)を保持することができます
Option<T>は、標準ライブラリで最もよく使われる列挙型であり、null の代わりとして機能しますmatchと併用する場合、コンパイラは 網羅的なチェック を強制しますOptionは、map、unwrap_or、expectなどの連鎖メソッドを提供しています。- 列挙型のメモリ使用量 ≈ 最大バリアントサイズ + 1 バイトのタグ
📝 練習問題
- 難易度 ⭐:月曜日から日曜日までを含む列挙型
Weekdayを定義してください。Weekdayを入力として受け取り、それが平日か週末かを(matchを使用して)返す関数を記述してください。 - 難易度 ⭐⭐: x >= 0 の場合は
fn safe_sqrt(x: f64) -> Option<f64>を返し、それ以外の場合はNoneを返す関数fn safe_sqrt(x: f64) -> Option<f64>を記述してください。main内で、正の数と負の数の両方をテストしてください。 - 難易度 ⭐⭐⭐:
Temperatureという列挙型を定義し、その中にCelsius(f64)とFahrenheit(f64)の 2 つのバリエーションを含めます。fn to_celsius(&self) -> f64およびfn to_fahrenheit(&self) -> f64メソッドを実装してください。mainでは、2 つの温度値を作成し、それらを相互に変換してください。



