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::Red
、Signal::Yellow
、Signal::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
バリアントにマッチした場合、その中のx
とy
フィールドの値が、そのままx
とy
という名前の変数に束縛(代入みたいなもの)されます。
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つあります。
それがOption
とResult
です。
これらも実は列挙型なんですよ!
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::
は、文字列s
をi32
型に変換しようとし、その結果を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
ブロックで列挙型にメソッドを定義できる。- 標準ライブラリの
Option
とResult
も列挙型で、Rustの安全性を支えている。
Rustの列挙型は、単なる値のリストではありません。データ構造と振る舞いを組み合わせ、状態を安全かつ表現力豊かにモデル化するための、まさにRustの「秘密兵器」とも言える機能です。
初めは少し戸惑うかもしれませんが、使いこなせるようになると、驚くほどコードが整理され、バグが減り、書くのが楽しくなるはずです!
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。