Rustのmatch式を完全攻略!条件分岐の基本からパターンマッチングの応用まで解説
生徒
「Rustの学習を始めたのですが、if文以外にもmatchという構文をよく見かけます。これは他の言語のswitchと同じものだと思えばいいですか?」
先生
「いい質問ですね!確かに他の言語のswitchに似ていますが、Rustのmatch式はそれよりも遥かに強力です。単なる値の比較だけでなく、データの構造を分解して取り出すパターンマッチングという機能が備わっているんですよ。」
生徒
「パターンマッチング……難しそうですね。具体的にどんなメリットがあるんですか?」
先生
「最大の特徴は、コンパイラが『すべてのケースを網羅しているか』を厳しくチェックしてくれる点です。つまり、考慮漏れによるバグを未然に防げるんです。基本から応用まで、一緒に見ていきましょう!」
1. Rustの強力な制御構文matchの基本概念
Rustにおける制御構文の中でも、特に頻繁に利用され、言語のアイデンティティとも言えるのがmatch式です。多くのプログラミング言語には、特定の条件に基づいて処理を分岐させるための仕組みとしてifやswitchが用意されています。しかし、Rustのmatchはこれらをさらに進化させたパターンマッチングという概念に基づいています。
matchは、ある一つの値を複数のパターンと比較し、最初に一致したパターンのコードブロックを実行します。このとき、Rustのコンパイラは「すべての可能性が考慮されているか(網羅性)」をチェックするため、想定外の状態によるクラッシュや論理エラーを劇的に減らすことが可能です。初心者の方はまず、「究極の条件分岐」として捉えると分かりやすいでしょう。
2. シンプルな数値や文字列によるパターンマッチング
まずは最も基本的な、整数値を使用した分岐を見てみましょう。match式は、対象となる値を中央に置き、それに対して枝(アーム)を伸ばしていくようなイメージで記述します。各アームは「パターン => 実行する処理」という形式で構成されます。
fn main() {
let number = 3;
match number {
1 => println!("1番です"),
2 => println!("2番です"),
3 => println!("3番です"),
_ => println!("それ以外です"),
}
}
3番です
上記のコードで重要なのは、最後に使われているアンダースコア(_)です。これは「ワイルドカード」と呼ばれ、それまでのパターンにどれも一致しなかった場合に実行されるデフォルトの処理を定義します。Rustでは、すべての値をカバーしなければコンパイルエラーになるため、数値のように無限の可能性がある型を扱う場合はこのワイルドカードが必須となります。これにより、開発者が特定のケースを処理し忘れるというミスを物理的に防いでいます。
3. 列挙型(Enum)とmatchの相性が抜群な理由
Rustの真価を発揮するのが、Enum(列挙型)とmatchを組み合わせたときです。Enumは「いくつかの決まった状態」を定義する型ですが、RustのEnumは各状態の中にデータを持たせることができます。matchを使うことで、その状態を判別するだけでなく、中身のデータを直接取り出して利用することが可能です。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn main() {
let msg = Message::Move { x: 10, y: 20 };
match msg {
Message::Quit => {
println!("終了します");
},
Message::Move { x, y } => {
println!("座標を x: {}, y: {} に移動しました", x, y);
},
Message::Write(text) => {
println!("メッセージ: {}", text);
},
}
}
座標を x: 10, y: 20 に移動しました
この例では、Message::Moveという列挙子の中に含まれるxとyという変数名をそのまま取り出しています。これを「パターンの解体(Destructuring)」と呼びます。他の言語であれば、まず型をチェックしてからキャストし、その後にプロパティにアクセスするという手順が必要になりますが、Rustではmatch一つで流れるように安全な処理が完結します。
4. Option型とResult型を安全に扱う手法
Rustには「値がない」ことを表すnullが存在しません。その代わりに、値があるかないかを表すOption型や、成功か失敗かを表すResult型を使用します。これらの型はすべてEnumとして定義されているため、matchを使って中身を取り出すのが標準的な作法です。
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Some(value) => println!("計算結果: {}", value),
None => println!("エラー: 0で割ることはできません"),
}
}
計算結果: 5
Option型を扱う際、Some(値)のケースとNoneのケースの両方を記述しない限り、Rustのコンパイラはプログラムを動かしてくれません。これにより、「値が入っている前提で処理を書いたけれど、実はnullだったので実行時に落ちてしまった」という、多くのプログラマーを悩ませてきた実行時エラーを、開発の段階で完全に排除できるのです。これがRustが「堅牢な言語」と呼ばれる所以の一つです。
5. マッチガードによる高度な条件フィルタリング
パターンの直後にif文を記述することで、さらに細かい条件をフィルタリングすることができます。これを「マッチガード」と呼びます。単なる値の一致だけでなく、「値が一致しており、かつこの条件を満たす場合」といった複雑なロジックを簡潔に表現できます。
fn main() {
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("xとyは等しいです"),
(x, y) if x + y == 0 => println!("足すと0になります"),
(x, _) if x % 2 == 0 => println!("xは偶数です"),
_ => println!("それ以外のケースです"),
}
}
足すと0になります
マッチガードを使用することで、パターンの表現力が飛躍的に高まります。タプルの中から特定の要素を取り出しつつ、その値の内容を動的にチェックする処理も、ネストした深いif文を書くことなく、フラットで読みやすいコードとして記述できるのが大きなメリットです。条件分岐が複雑になりがちなゲーム開発やシステム構築において、この見通しの良さは非常に強力な武器となります。
6. 範囲指定や複数パターンの結合による効率化
matchのパターンには、単一の値だけでなく、範囲(レンジ)や複数の候補を指定することも可能です。これにより、冗長なコードを減らし、意図が伝わりやすい洗練されたプログラムを記述できます。例えば、「1から5までの数値」や「'A'または'B'」といった条件を、一つのアームでまとめて処理できるのです。
fn main() {
let char_code = 'k';
match char_code {
'a'..='z' => println!("小文字のアルファベットです"),
'A'..='Z' => println!("大文字のアルファベットです"),
'0'..='9' => println!("数字です"),
_ => println!("それ以外の文字です"),
}
}
小文字のアルファベットです
..=という記号を使うことで、終端を含む範囲を指定できます。また、パイプ記号(|)を使えば「または」という意味で複数のパターンを結合することも可能です。これらの機能を活用することで、大量の条件分岐が必要な場面でも、コードの可読性を損なうことなく、簡潔にロジックを詰め込むことができます。初心者のうちは、まずはこの範囲指定から使いこなせるようになると、Rustらしいコードが書けるようになっていきます。
7. 式としてのmatchを利用した変数への代入
Rustにおいて、matchは「文」ではなく「式」です。これは、matchの結果をそのまま値として変数に代入できることを意味します。他の言語では、変数を先に宣言しておき、分岐の中でその変数に値を書き込むという手順を踏むことが多いですが、Rustではより直接的な記述が可能です。
fn main() {
let boolean = true;
let binary = match boolean {
false => 0,
true => 1,
};
println!("結果は {} です", binary);
}
結果は 1 です
この書き方の利点は、変数を「不変(immutable)」として扱える点にあります。値を後から書き換える必要がないため、プログラムの動作が予測しやすくなり、バグの混入を防ぐことができます。また、各アームが返す値の型はすべて一致している必要があるため、型の不整合もコンパイル時に検知されます。このように、matchを「式」として扱う文化は、Rustの安全で関数型プログラミングに近い特性を象徴していると言えるでしょう。
8. matchとif letの使い分けについて
matchは非常に強力ですが、時には「特定の1つのケースだけを処理したい」という場面もあります。その場合、他のすべてのパターンに対して_ => ()(何もしない)と書くのは少々手間です。そこで役立つのがif letという糖衣構文(シンタックスシュガー)です。これは特定のパターンに一致したときだけ処理を行い、それ以外を無視する簡略化された書き方です。
しかし、if letを使うと、matchが持っていた「網羅性のチェック」という強力なガードレールが失われてしまいます。そのため、将来的にEnumに新しい状態が追加されたときなど、matchを使っていればコンパイルエラーで気づけたはずの修正箇所を見逃してしまうリスクがあります。基本的な指針としては、複数の状態を厳密に分けたい場合はmatchを使い、特定の条件以外に興味がない場合や、コードを極限まで短くしたい場合にのみif letを検討するのが良いでしょう。この使い分けができるようになれば、あなたも立派なRustの初級卒業と言えます。