matchはRustを学び始めると必ず出会う、ちょっと変わった、でもめちゃくちゃ便利な制御構文です。
他の言語のif文やswitch文に慣れていると、「なんだこりゃ?」って思うかもしれませんね。
この記事では、Rustのmatch式の基本から、ちょっと応用的な使い方まで、プログラミング初心者の方にも「なるほど!」と思っていただけるように、サンプルコードをたくさん使って解説していきます。
この記事で学べること
- matchがどんなものか、基本的な考え方
- matchの書き方のルール(構文)
- 便利なパターンの使い方(変数束縛、ワイルドカード、ORパターン)
- Rustでよく使う
Option
型やResult
型をmatchで扱う方法 - matchを使うときに気をつけるポイント
Rustのmatchとは?他の言語の条件分岐との違い
さて、まずは「matchって何なのさ?」という疑問から解決しましょう!
一言でいうと、matchは「値と『パターン』を比較して、一致した『アーム』の処理を実行する」ための仕組みです。
他のプログラミング言語、例えばC言語やJava、Pythonなんかには、条件によって処理を分けるためにif/else
文やswitch/case
文がありますよね。
Rustのmatchは、それらに似ているようで、実はもっとパワフルなんです。
何が違うかというと、一番大きな特徴は「網羅性チェック」があること。
これは、「考えられる全てのパターンをちゃんと処理するように!」とRustコンパイラが見張ってくれている機能です。もし書き漏れがあったら、コンパイル時に「おい、ここ処理書いてないぞ!」って教えてくれる親切設計。これにより、うっかりミスによるバグを防ぎやすくなるんですよ。
例えば、信号機の色を判定する場合、
- 赤の場合
- 黄の場合
- 青の場合
この3パターンを全部書かないと、コンパイラに怒られちゃう、というイメージです。
この「全部の可能性を考えなさい」というルールが、プログラムの安全性を高めているんですね。
Rustのmatchの基本的な書き方(構文)
matchの正体がなんとなく分かったところで、次は実際の書き方を見ていきましょう。
基本の形はこんな感じです。
match 値 { パターン1 => 式1, パターン2 => 式2, パターン3 => 式3, // ... 他のパターンと式 ... _ => 式N, // ワイルドカードパターン (後で説明します) }
分解してみましょう。
match
: 「これからmatch式を始めますよー」という合図のキーワード。値
: 比較したい変数やリテラル(具体的な数字や文字列など)を書きます。{ ... }
: 波括弧の中に、条件分岐のルール(アーム)を書いていきます。パターン => 式,
: 「アーム」と呼ばれます。左側の「パターン」に「値」が一致したら、右側の「式」が実行されます。アームの最後はカンマ,
で区切ります(最後のアームのカンマは省略してもOK)。=>
: パターンと実行する式を結びつける矢印みたいな記号です。「ファットアロー」なんて呼ばれたりも。
最初はちょっと見慣れないかもしれませんが、大丈夫!すぐに慣れますよ。
シンプルな値とのマッチング
まずは、一番簡単な例から見てみましょう。数字のマッチングです。// --- ソースコード ---
fn main() { let number = 3; match number { 1 => println!("数字は1です!"), 2 => println!("数字は2だね。"), 3 => println!("おっ、数字は3でした!"), // ここがマッチするはず! _ => println!("1, 2, 3以外の数字だよ。"), // それ以外の場合 } }
// --- ソースコードの表示結果 --- おっ、数字は3でした!
上のコードでは、変数number
の値(ここでは3)をmatch
で調べています。
match
は上から順番にパターンをチェックしていき…
1
とnumber
(3) は違う。2
とnumber
(3) も違う。3
とnumber
(3) は… 一致!
というわけで、3 => println!("おっ、数字は3でした!"),
のアームが実行され、コンソールにメッセージが表示されました。
最後の _ => ...
は、どのパターンにも一致しなかった場合に実行される特別なアームです。これについては、後ほど詳しく説明しますね。
matchで使える様々なパターン入門
matchのすごいところは、ただ単に「値が同じかどうか」を比べるだけじゃない点にあります。
もっと複雑な「パターン」を使って、より柔軟な条件分岐ができるんです。ここからは、初心者の方がまず押さえておきたい基本的なパターンをいくつか紹介しますね!
値を変数に束縛する
パターンに一致した時、その値を一時的な変数に入れて、後の処理で使いたい場合がありますよね。
そんな時に使うのが「変数束縛」です。
パターンの中に変数名を書くだけで、マッチした値がその変数に代入(束縛)されます。
fn main() { let value = Some(10); // Option型 (後で説明します) match value { Some(x) => { // Someの中の値がxに束縛される println!("値が見つかりました!中身は {} です。", x); // 束縛した変数xを使える }, None => { println!("値はありませんでした。"); }, } }
// --- ソースコードの表示結果 --- 値が見つかりました!中身は 10 です。
上の例では、value
がSome(10)
という値を持っています。
match
式のSome(x)
というパターンに注目してください。
value
がSome(...)
の形にマッチしたので、その中身である10
が変数x
に束縛されました。
そして、=>
の右側の処理で、その変数x
を使ってメッセージを表示しています。
このように、マッチした値を変数に入れて後で使えるのが変数束縛の便利な点です。
全ての可能性を拾う - ワイルドカード _
先ほどの例にも出てきましたが、_
(アンダースコア)は特別なパターンで、「どんな値にもマッチする」という意味を持ちます。
これは「ワイルドカード」と呼ばれます。
match式では、考えられる全てのパターンを網羅しないとコンパイルエラーになるんでしたね。
でも、「特定のいくつかのケース以外は、全部同じ処理でいいや」という場合も多いです。
そんな時に_
が大活躍!
_
をmatch式の最後に書いておけば、「上記以外の全てのパターン」を拾ってくれるので、網羅性チェックをクリアできるのです。
fn main() { let number = 5; match number { 1 => println!("いち!"), 2 => println!("に!"), // 3や4のパターンは書いていないけど... _ => println!("3以上、または1,2以外の数字だね!"), // 1, 2以外は全部ここにマッチ } }
// --- ソースコードの表示結果 --- 3以上、または1,2以外の数字だね!
この例では、number
が5なので、1
にも2
にもマッチしません。
そこで、最後の_
パターンが「それ以外全部」をキャッチし、対応するメッセージが表示されました。
もし_
パターンがないと、「パターン3
, 4
, 5
, ... がカバーされてないよ!」とコンパイラに怒られてしまいます。
ワイルドカード_
は、網羅性を満たすための必須テクニックと言えるでしょう。
複数のパターンを一度に - ORパターン |
「このパターンか、あるいは、あのパターンの場合に、同じ処理をしたい」
そんな時には、|
(縦棒、パイプ記号)を使って複数のパターンを繋げられます。これをORパターンと呼びます。
fn main() { let character = 'k'; match character { 'a' | 'i' | 'u' | 'e' | 'o' => { // 'a' または 'i' または ... println!("{} は母音だね。", character); }, 'k' | 's' | 't' | 'n' | 'h' => { // 'k' または 's' または ... println!("{} は子音っぽい!", character); // ここにマッチ! }, _ => { println!("{} はアルファベットの母音でも子音でもないみたい。", character); }, } }
// --- ソースコードの表示結果 --- k は子音っぽい!
この例では、character
が'k'
なので、'k' | 's' | 't' | 'n' | 'h'
のパターンにマッチし、対応するメッセージが表示されました。
|
を使えば、同じ処理をまとめられるので、コードがスッキリしますね!
Rustのmatchの実践的な使い方 - Option<T>
とResult<T, E>
さて、ここからはmatchが特に輝く場面、Rustプログラミングで避けては通れないOption
型とResult
型の扱い方を見ていきましょう。
「値があるかもしれないし、ないかもしれない」「処理が成功したかもしれないし、失敗したかもしれない」
そんな状況を安全に扱うための仕組みで、matchとの相性が抜群なんです!
Option<T>
型の値を安全に取り出す
Option<T>
型は、値が存在する可能性(Some(値)
)と、存在しない可能性(None
)のどちらかを表す型です。
例えば、配列から要素を探す関数は、見つかればSome(要素)
を、見つからなければNone
を返す、といった具合に使われます。
Option
型の値を安全に扱うには、matchを使ってSome
の場合とNone
の場合の両方をきちんと処理するのが定石です。
fn find_first_even(numbers: Vec<i32>) -> Option<i32> { for num in numbers { if num % 2 == 0 { return Some(num); // 最初に見つかった偶数をSomeで包んで返す } } None // 見つからなければNoneを返す } fn main() { let nums1 = vec![1, 3, 5, 6, 7]; let nums2 = vec![1, 3, 5, 7, 9]; let first_even1 = find_first_even(nums1); match first_even1 { Some(n) => println!("nums1 で最初の偶数発見!それは {} です。", n), // Some(6) にマッチ None => println!("nums1 には偶数がありませんでした。"), } let first_even2 = find_first_even(nums2); match first_even2 { Some(n) => println!("nums2 で最初の偶数発見!それは {} です。", n), None => println!("nums2 には偶数がありませんでした。"), // None にマッチ } }
// --- ソースコードの表示結果 --- nums1 で最初の偶数発見!それは 6 です。 nums2 には偶数がありませんでした。
find_first_even
関数は、整数のベクタ(配列みたいなもの)を受け取り、最初の偶数を見つけたらSome(偶数)
を、見つけられなかったらNone
を返します。
main
関数では、それぞれの結果をmatch
で判定しています。
Some(n)
パターンで値を変数n
に束縛して利用したり、None
パターンで値がなかった場合の処理をしたりしていますね。
Option
とmatchの組み合わせは、値がないかもしれない状況を安全に取り扱うための基本パターンです。
Result<T, E>
型で成功と失敗を処理する
Result<T, E>
型は、処理が成功した場合(Ok(成功時の値)
)と、失敗した場合(Err(失敗時のエラー情報)
)のどちらかを表す型です。
ファイルを開く、ネットワーク通信をするなど、失敗する可能性のある操作の多くがResult
型を返します。
これもOption
と同様に、matchを使ってOk
の場合とErr
の場合を分けて処理するのが一般的です。
// 簡単な割り算関数 (0除算はエラーとする) fn safe_divide(numerator: f64, denominator: f64) -> Result<f64, String> { if denominator == 0.0 { Err("ゼロで割ることはできません!".to_string()) // Errでエラーメッセージを返す } else { Ok(numerator / denominator) // Okで計算結果を返す } } fn main() { let result1 = safe_divide(10.0, 2.0); // 成功するはず match result1 { Ok(value) => println!("割り算成功! 結果: {}", value), // Ok(5.0) にマッチ Err(e) => println!("割り算失敗… エラー: {}", e), } let result2 = safe_divide(5.0, 0.0); // 失敗するはず (ゼロ除算) match result2 { Ok(value) => println!("割り算成功! 結果: {}", value), Err(e) => println!("割り算失敗… エラー: {}", e), // Err("ゼロで割ることはできません!") にマッチ } }
// --- ソースコードの表示結果 --- 割り算成功! 結果: 5 割り算失敗… エラー: ゼロで割ることはできません!
safe_divide
関数は、割り算の結果をResult
で返します。ゼロ除算の場合はErr
を、そうでなければOk
を返します。
main
関数では、それぞれのResult
をmatch
で受け取り、成功時(Ok(value)
)と失敗時(Err(e)
)で処理を分岐させていますね。
Result
とmatchの組み合わせは、エラーハンドリングの基本形であり、堅牢なプログラムを作る上で欠かせません。
Rustのmatchを使う上での注意点
matchは非常に便利ですが、いくつか気をつけておきたい点もあります。
一番のポイントは、やはり「網羅性」です。
match
で調べる値が取りうる全ての可能性に対して、パターンを用意する必要があります。
例えば、bool型(true
かfalse
のどちらか)の値をmatch
にかけるなら、
match is_active { true => ..., false => ..., }
のように、true
の場合とfalse
の場合、両方のアームを書く必要があります。(_
を使っても網羅できますが、boolなら普通両方書きますね)
もし書き忘れると、コンパイラが「このパターン抜けてるよ!」と教えてくれるので、実行時エラーを防ぐ手助けになります。面倒に感じるかもしれませんが、プログラムの安全性を高めるための、Rustの優しさだと思ってください!
もう一つ、matchは「式」であるという点も覚えておくと良いでしょう。
式ということは、値を返すことができるということです。例えば、matchの結果を変数に入れることもできます。
let message = match status_code { 200 => "成功しました!", 404 => "見つかりません。", _ => "その他のステータスです。", }; println!("{}", message); // message変数にmatchの結果が入る
各アームの=>
の右側にある式が返す値の型は、全て同じ(または互換性のある)型である必要があります。
【まとめ】matchでRustらしいコードを書こう
お疲れ様でした!Rustのmatch
について、基本的なところから実践的な使い方まで見てきました。
ポイントを振り返ってみましょう。
- matchは値とパターンを比較して処理を分岐する仕組み。
- 網羅性チェックがあるので、全ての可能性を考慮する必要がある(
_
が役立つ)。 - パターンマッチングで、値の取り出し(変数束縛)や複数条件の指定(ORパターン)ができる。
Option
型やResult
型を安全に扱うのにmatchは最適。- matchは式なので、値を返すことができる。
最初は少し戸惑うかもしれませんが、matchを使いこなせるようになると、Rustのコードがぐっと読みやすく、そして安全になります。まさにRustらしさを体現する機能の一つと言えるでしょう。
ぜひ、ご自身のコードでも積極的にmatchを使ってみてください。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。