Rustのパターンマッチング
matchのページでは、アーム、_、| で値をまとめる方法、範囲といった基本を学びました。でも、それはほんの始まりです。Rustのパターンは、値の中身をのぞいて、必要な部分だけを取り出し、さらに追加の条件までチェックできます。しかも、それを1行で書けるのです。
パターンは、クッキーの型抜きのようなものだと考えてください。型を値に押し当てて、形が合えば、Rustが中身のパーツを渡してくれます。合わなければ、次のアームを試します。このイメージがつかめると、match、let、関数の引数、if letなど、あらゆるところでパターンを使うようになります。
マッチガード:if を付け足す
Section titled “マッチガード:if を付け足す”形が合うだけでは足りず、もう1つチェックしたいこともあります。マッチガード(match guard)は、パターンの後ろに書く if の条件です。パターンに当てはまり、かつ条件が真のときだけ、そのアームが実行されます。
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "凍えるほど寒い", 0 => "ちょうど0度", t if t < 25 => "快適", _ => "暑い", }; println!("{t}°Cは{feeling}"); }}実行結果:
-5°Cは凍えるほど寒い0°Cはちょうど0度18°Cは快適35°Cは暑いRustはアームを上から順にチェックするので、順番が大切です。18 は最初のガードを通らず、0 でもなく、t < 25 を満たすので「快適」になります。
タプルと構造体の分解
Section titled “タプルと構造体の分解”分解(destructuring)とは、値をパーツに分けることです。タプルなら、タプルと同じ形でパターンを書きます。構造体なら、取り出したいフィールドの名前を書きます。
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("原点"), (x, 0) => println!("x軸上の{x}"), (0, y) => println!("y軸上の{y}"), (x, y) => println!("それ以外の場所: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("y軸上、y = {y}"), Point { x, y: 0 } => println!("x軸上、x = {x}"), Point { x, y } => println!("位置: ({x}, {y})"), }}実行結果:
それ以外の場所: (3, -7)y軸上、y = 52種類のものが混ざっていることに注目してください。0 は決まった値で、ぴったり一致しなければなりません。x や y は新しい変数で、そこにある値が何であれ受け止めます。Point { x, y } は Point { x: x, y: y } の省略形です。
データを持つenumの分解
Section titled “データを持つenumの分解”パターンが本当に活躍するのはここです。列挙型(enum)のバリアントはデータを持てます。パターンを使うと、どのバリアントなのかをチェックしながら、そのデータを取り出せます。
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("面積 = {:.2}", area(shape)); }}実行結果:
面積 = 12.57面積 = 13.50面積 = 2.25値が Circle だと確かめない限り、radius には手が届きません。これが安全性の面での大きな利点です。違うバリアントからデータを読み出そうとしても、コンパイラが許してくれません。
入れ子のパターンと @ 束縛
Section titled “入れ子のパターンと @ 束縛”パターンの中に別のパターンを、好きなだけ深く入れられます。次の例では、Command というenumが Direction というenumを持っていて、1つのアームで両方の階層をいっぺんにマッチしています。
@ 束縛(@ binding)を使うと、値がパターンに当てはまるかを確かめ、さらにその値を変数に入れておけます。n @ 1..=9 は「1から9の間にあるかを確かめて、それを n と呼ぶ」という意味です。
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("上へ{steps}(小さな一歩)"), Command::Move(Direction::Up, steps) => println!("上へ{steps}(大ジャンプ!)"), Command::Move(Direction::Down, steps) => println!("下へ{steps}"), Command::Say(text) => println!("発言: {text}"), Command::Quit => println!("さようなら"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("こんにちは"))); run(Command::Quit);}実行結果:
上へ3(小さな一歩)上へ50(大ジャンプ!)下へ2発言: こんにちはさようなら@ がなければ、1..=9 のような範囲は値をチェックするだけで、その値は捨てられてしまいます。@ を使えば、値を手元に残しておけます。
_ と .. で一部を無視する
Section titled “_ と .. で一部を無視する”パーツの一部にしか興味がないこともよくあります。値を1つ飛ばすには _ を、「残りすべて」を飛ばすには .. を使います。
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("赤 = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("最初 = {first}、最後 = {last}");
let hero = Player { name: String::from("ミラ"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name}のレベルは{level}");}実行結果:
赤 = 255最初 = 1、最後 = 5ミラのレベルは7.. は1つのパターンの中で1回しか使えません。(first, .., last) は大丈夫ですが、(.., middle, ..) はだめです。これではRustが、どの値のことを言っているのか判断できないからです。
let と関数の引数でのパターン
Section titled “let と関数の引数でのパターン”実は、どの let もパターンです。関数の引数もそうです。つまり、値を受け取ったその場で分解できるのです。
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}実行結果:
1920x1080x: 4, y: -2[3, 7, 11]クロージャ |&(a, b)| a + b も同じ考え方です。各要素は &(i32, i32) で、パターンがそれをそのまま a と b に分解しています。イテレータを使うときに、よく目にする書き方です。
matches! とlet-else
Section titled “matches! とlet-else”match を全部書かずに済む、便利な道具が2つあります。
matches!(値, パターン) は true か false を返します。「はい/いいえ」だけわかればよいときに、if や .filter() の中で使うのにぴったりです。ここでもガードが使えます。
let-else は「これを取り出す。できなければ、ここで抜ける」という処理を書くためのものです。let パターン = 値 else { ... }; と書きます。パターンに当てはまれば、その変数を関数の残りの部分で使えます。当てはまらなければ else ブロックが実行され、そのブロックは return、break、continue、またはパニックで必ず抜けなければなりません。
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("「{input}」は数値ではありません"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("合格者は{passed}人");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("三十一"));}実行結果:
true合格者は3人Some(31)「三十一」は数値ではありませんNonelet-elseを使うと、うまくいったときの流れ(ハッピーパス)が平らに保てます。コードを if let ... { } の中に入れ子にする代わりに、まずうまくいかないケースを片付けてから先へ進めるのです。OptionやResultの値と組み合わせて使いましょう。
よくある間違い
Section titled “よくある間違い”ガードは「すべてのケースを網羅しているか」の判定に数えられません。 コンパイラは if の条件の中身までは推論できません。そのため、次のコードは2つのガードであらゆる数をカバーしているのに、エラーになります。
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("負の数"), x if x >= 0 => println!("0または正の数"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityこのエラーは「パターンが網羅されていない。i32::MIN..=i32::MAX(i32 のすべての値)がカバーされていない」という意味です。最後の注記には「ガード付きのアームは網羅性の判定に数えない」と書かれています。直し方は、最後のアームをガードなしの「それ以外すべて」にすることです。たとえば _ => println!("0または正の数") のようにします。
パターンの中の変数名は、比較ではなく新しい変数です。 次のコードは guess == expected をチェックしているように見えますが、実はそうではありません。
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("正解!"), _ => println!("不正解"), }}これはコンパイルでき、正解! と表示されます。アームに書いた expected は、どんな値でも受け止める新しい変数を作っているだけだからです。Rustは次のように警告してくれます。
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("正解!"), | -------- matches any value6 | _ => println!("不正解"), | ^ no value can reach this「到達できないパターン」という警告で、expected が「どんな値にも当てはまる」ため、_ には「どの値も届かない」と教えてくれています。直し方はガードを使うことです。g if g == expected => println!("正解!") のように書きます。
1. タプルで書くFizzBuzz
Section titled “1. タプルで書くFizzBuzz”9から15までの数を表示します。ただし、3の倍数のときは Fizz、5の倍数のときは Buzz、両方の倍数のときは FizzBuzz と表示してください。if を連ねるのではなく、タプルに対する match を1つだけ使って書きましょう。ヒント:(n % 3, n % 5) でマッチします。余りが 0 なら「割り切れる」、_ なら「どちらでもよい」という意味になります。
解答を見る
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}実行結果:
FizzBuzz11Fizz1314FizzBuzz(0, 0) は最初に書く必要があります。もし (0, _) が先にあると、15のときに Fizz と表示されてしまいます。
2. 銀行の取引
Section titled “2. 銀行の取引”enum Transaction { Deposit(u32), Withdraw(u32), Fee } を定義します。balance(残高)を 100 から始めて、次の取引を順番に適用してください:50の入金、30の引き出し、500の引き出し、手数料。手数料は5かかります。残高より大きい引き出しは、メッセージを表示して却下しなければなりません。最後に残高を表示しましょう。ヒント:マッチガード Transaction::Withdraw(amount) if amount <= balance を使い、その後ろに却下用の Withdraw アームをもう1つ書きます。
解答を見る
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("却下: {amount}は引き出せません"), Transaction::Fee => balance -= 5, } }
println!("残高 = {balance}");}実行結果:
却下: 500は引き出せません残高 = 1152つ目の Withdraw アームにはガードがないので、1つ目のアームが断った引き出しをすべて受け止めます。このアームがないと、match はコンパイルできません。Rustがすべてのケースを網羅しているかをチェックするとき、ガードは数えられないからです。
- マッチガード(
パターン if 条件)で、アームに追加のチェックを付けられます。 - パターンはタプル、構造体、enumのバリアントを分解でき、入れ子にもできます。
名前 @ パターンは、値をチェックすると同時に、その値を変数に入れておけます。_は値を1つ飛ばし、..は残りを飛ばします。パターンはlet、関数の引数、クロージャでも使えます。matches!は手軽にtrue/falseを返し、let-elseは値を取り出すか、早めに抜けるかを書けます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. matchのアームで n @ 1..=9 はどんな働きをしますか?
2. let-elseで、else ブロックは何をしなければなりませんか?
3. let (first, .., last) = (1, 2, 3, 4, 5); を実行すると、どうなりますか?
4. ガード付きのアームだけで書いた match が、よくコンパイルエラーになるのはなぜですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- パターンとマッチング — The Rust Programming Language 日本語版:パターンが使えるすべての場所と、パターンがマッチに失敗しうるケースを解説した公式の章です。
- パターン記法 — The Rust Programming Language 日本語版:範囲や分解から、ガード、
@束縛まで、あらゆるパターンの書き方を一通り紹介しています。 - Staying on the Happy Path with let…else — The Rust Book(英語):let-elseを、具体的な例でやさしく説明しています。
- デストラクト — Rust By Example 日本語版:タプル、配列、enum、ポインタ、構造体を分解する、実行できるサンプル集です。
matches!マクロ — 標準ライブラリのドキュメント(英語):ガードの付け方も含めた、このマクロの正確な書き方がわかります。