コンテンツにスキップ

Rustのパターンマッチング

matchのページでは、アーム、_、| で値をまとめる方法、範囲といった基本を学びました。でも、それはほんの始まりです。Rustのパターンは、値の中身をのぞいて、必要な部分だけを取り出し、さらに追加の条件までチェックできます。しかも、それを1行で書けるのです。

パターンは、クッキーの型抜きのようなものだと考えてください。型を値に押し当てて、形が合えば、Rustが中身のパーツを渡してくれます。合わなければ、次のアームを試します。このイメージがつかめると、match、let、関数の引数、if letなど、あらゆるところでパターンを使うようになります。

形が合うだけでは足りず、もう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 を満たすので「快適」になります。

分解(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 = 5
▶ 実際に試してみよう

2種類のものが混ざっていることに注目してください。0 は決まった値で、ぴったり一致しなければなりません。x や y は新しい変数で、そこにある値が何であれ受け止めます。Point { x, y } は Point { x: x, y: y } の省略形です。

パターンが本当に活躍するのはここです。列挙型(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 には手が届きません。これが安全性の面での大きな利点です。違うバリアントからデータを読み出そうとしても、コンパイラが許してくれません。

パターンの中に別のパターンを、好きなだけ深く入れられます。次の例では、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 のような範囲は値をチェックするだけで、その値は捨てられてしまいます。@ を使えば、値を手元に残しておけます。

パーツの一部にしか興味がないこともよくあります。値を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 もパターンです。関数の引数もそうです。つまり、値を受け取ったその場で分解できるのです。

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);
}

実行結果:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ 実際に試してみよう

クロージャ |&(a, b)| a + b も同じ考え方です。各要素は &(i32, i32) で、パターンがそれをそのまま a と b に分解しています。イテレータを使うときに、よく目にする書き方です。

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)
「三十一」は数値ではありません
None
▶ 実際に試してみよう

let-elseを使うと、うまくいったときの流れ(ハッピーパス)が平らに保てます。コードを if let ... { } の中に入れ子にする代わりに、まずうまくいかないケースを片付けてから先へ進めるのです。OptionやResultの値と組み合わせて使いましょう。

ガードは「すべてのケースを網羅しているか」の判定に数えられません。 コンパイラは 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 value
6 | _ => println!("不正解"),
| ^ no value can reach this

「到達できないパターン」という警告で、expected が「どんな値にも当てはまる」ため、_ には「どの値も届かない」と教えてくれています。直し方はガードを使うことです。g if g == expected => println!("正解!") のように書きます。

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}"),
}
}
}

実行結果:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ 実際に試してみよう

(0, 0) は最初に書く必要があります。もし (0, _) が先にあると、15のときに Fizz と表示されてしまいます。

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は引き出せません
残高 = 115
▶ 実際に試してみよう

2つ目の Withdraw アームにはガードがないので、1つ目のアームが断った引き出しをすべて受け止めます。このアームがないと、match はコンパイルできません。Rustがすべてのケースを網羅しているかをチェックするとき、ガードは数えられないからです。

  • マッチガード(パターン if 条件)で、アームに追加のチェックを付けられます。
  • パターンはタプル、構造体、enumのバリアントを分解でき、入れ子にもできます。
  • 名前 @ パターン は、値をチェックすると同時に、その値を変数に入れておけます。
  • _ は値を1つ飛ばし、.. は残りを飛ばします。パターンは let、関数の引数、クロージャでも使えます。
  • matches! は手軽に true / false を返し、let-elseは値を取り出すか、早めに抜けるかを書けます。

理解度チェック

1. matchのアームで n @ 1..=9 はどんな働きをしますか?

2. let-elseで、else ブロックは何をしなければなりませんか?

3. let (first, .., last) = (1, 2, 3, 4, 5); を実行すると、どうなりますか?

4. ガード付きのアームだけで書いた match が、よくコンパイルエラーになるのはなぜですか?

スコア: 0 / 4