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 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。