列挙型(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 の場合の処理が書かれていない! }
}
すべてのヴァリアントを列挙するのが大変な場合や、特定のパターン以外は同じ処理をしたい場合は、特殊なパターン _
(アンダースコア)を使うことができます。_
は「それ以外のすべての値」にマッチするワイルドカードとして機能します。
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の安全性と表現力を支える重要な機能です。ぜひ使いこなせるようになりましょう!