列挙型(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の安全性と表現力を支える重要な機能です。ぜひ使いこなせるようになりましょう!