パターンの構文
この節では、パターンで有効な構文をすべて集め、それぞれをなぜ、そしてどのようなときに使いたくなるのかを説明します。
リテラルとのマッチング
第6章で見たように、パターンをリテラルに対して直接マッチさせることができます。次のコードはその例です。
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
このコードは、x の値が 1 なので one を出力します。特定の具体的な値を受け取った場合にコードに何らかの動作をさせたいとき、この構文は役に立ちます。
名前付き変数とのマッチング
名前付き変数はどんな値にもマッチする反駁不能なパターンであり、本書でも何度も使ってきました。しかし、名前付き変数を match、if let、while let 式で使う場合には、少し注意が必要です。これらの式はいずれも新しいスコープを開始するため、これらの式の内部でパターンの一部として宣言された変数は、ほかの変数と同様に、構文の外側にある同名の変数をシャドーイングします。リスト19-11では、Some(5) という値を持つ x という変数と、10 という値を持つ y という変数を宣言しています。次に、値 x に対する match 式を作成します。matchアーム内のパターンと最後の println! を見て、このコードを実行したり先を読んだりする前に、何が出力されるか考えてみてください。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match 式が実行されると何が起きるかを追ってみましょう。最初のmatchアームのパターンは、定義されている x の値にマッチしないため、コードは次に進みます。
2番目のmatchアームのパターンは、Some 値の内側にある任意の値にマッチする、新しい y という名前の変数を導入します。ここでは match 式の内側という新しいスコープにいるため、これは新しい y 変数であり、冒頭で値 10 として宣言した y ではありません。この新しい y への束縛は、Some の内側にあるどんな値にもマッチしますが、x がまさにそれです。そのため、この新しい y は x 内の Some の内側の値に束縛されます。その値は 5 なので、このアームの式が実行され、Matched, y = 5 が出力されます。
もし x が Some(5) ではなく None 値だったなら、最初の2つのアームのパターンはマッチしないので、その値はアンダースコアにマッチしたはずです。アンダースコアのアームのパターンでは x 変数を導入していないので、その式にある x は、依然としてシャドーイングされていない外側の x です。この仮定の場合、match は Default case, x = None を出力します。
match 式が終わると、そのスコープも終わり、内側の y のスコープも同様に終わります。最後の println! は at the end: x = Some(5), y = 10 を出力します。
既存の y 変数をシャドーイングする新しい変数を導入するのではなく、外側の x と y の値を比較する match 式を作るには、代わりに match ガード条件を使う必要があります。match ガードについては、後の 「match ガードで条件を追加する」 節で説明します。
複数のパターンとのマッチング
match 式では、パターンの or 演算子である | 構文を使って、複数のパターンにマッチさせることができます。たとえば、次のコードでは x の値をmatchアームに対してマッチさせていますが、その最初のアームには or の選択肢があります。これは、x の値がそのアームにあるどちらかの値にマッチすれば、そのアームのコードが実行されることを意味します。
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
このコードは one or two を出力します。
..= を使った値の範囲とのマッチング
..= 構文を使うと、両端を含む値の範囲にマッチさせることができます。次のコードでは、パターンが与えられた範囲内のいずれかの値にマッチすると、そのアームが実行されます。
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
x が 1、2、3、4、5 のいずれかであれば、最初のアームにマッチします。この構文は、同じ考えを | 演算子で表現するよりも、複数のマッチ値に対して便利です。| を使うとしたら、1 | 2 | 3 | 4 | 5 と指定しなければなりません。範囲を指定するほうがずっと短く、とりわけ 1 から 1,000 までの任意の数にマッチさせたいような場合には便利です。
コンパイラはコンパイル時にその範囲が空でないことを検査します。また、Rust が範囲が空かどうかを判定できる型は char と数値だけなので、範囲は数値または char の値に対してのみ許可されます。
char 値の範囲を使う例を次に示します。
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust は 'c' が最初のパターンの範囲内にあると判断できるので、early ASCII letter を出力します。
デストラクチャリングで値を分解する
パターンを使って構造体、列挙型、タプルをデストラクチャリングし、それらの値のさまざまな部分を利用することもできます。それぞれの値について見ていきましょう。
構造体
リスト19-12は、x と y という2つのフィールドを持つ Point 構造体を示しています。これは、let 文でパターンを使って分解できます。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
このコードは、p 構造体の x フィールドと y フィールドの値にマッチする a と b という変数を作成します。この例は、パターン内の変数名が構造体のフィールド名と一致している必要はないことを示しています。しかし、どの変数がどのフィールドから来たのかを覚えやすくするために、変数名をフィールド名に合わせるのが一般的です。このような一般的な使い方があること、そして let Point { x: x, y: y } = p; と書くと重複が多いことから、Rust には構造体フィールドにマッチするパターンの短縮記法があります。構造体フィールドの名前だけを列挙すればよく、パターンから作られる変数は同じ名前になります。リスト19-13はリスト19-12のコードと同じように動作しますが、let パターンで作成される変数は a と b ではなく x と y です。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
このコードは、変数 p の x フィールドと y フィールドにマッチする x と y という変数を作成します。その結果、変数 x と y には p 構造体の値が入ります。
構造体パターンの一部としてリテラル値を使って分解することもできます。こうすると、すべてのフィールドについて変数を作成するのではなく、一部のフィールドが特定の値を持つかどうかをテストしつつ、ほかのフィールドについては分解して変数を作成できます。
リスト 19-14 では、match 式を使って Point の値を 3 つのケースに分けています。x 軸上にある点(y = 0 のときに成り立つ)、y 軸上にある点(x = 0)、そしてどちらの軸上にもない点です。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
最初のアームは、y フィールドがリテラル 0 にマッチすることを指定することで、x 軸上にある任意の点にマッチします。このパターンでは、引き続き x 変数も作成されるため、このアームのコード内でそれを使えます。
同様に、2 番目のアームは、x フィールドの値が 0 であることを指定することで、y 軸上の任意の点にマッチし、y フィールドの値に対する変数 y を作成します。3 番目のアームではリテラルをまったく指定していないため、ほかのあらゆる Point にマッチし、x フィールドと y フィールドの両方に対して変数を作成します。
この例では、p の値は x に 0 が入っているため、2 番目のアームにマッチします。したがって、このコードは On the y axis at 7 を出力します。
match 式は最初にマッチするパターンを見つけた時点でアームのチェックをやめることを思い出してください。そのため、Point { x: 0, y: 0 } は x 軸上でも y 軸上でもありますが、このコードが出力するのは On the x axis at 0 だけです。
列挙型
本書ではこれまでも列挙型を分解してきました(たとえば第 6 章のリスト 6-5)。しかし、列挙型を分解するためのパターンが、その列挙型の内部に格納されているデータの定義方法に対応していることは、まだ明示的には説明していませんでした。例として、リスト 19-15 ではリスト 6-2 の Message 列挙型を使い、それぞれの内部の値を分解するパターンを持つ match を書いています。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
このコードは Change color to red 0, green 160, and blue 255 を出力します。msg の値を変更して、ほかのアームのコードが実行されるのを試してみてください。
Message::Quit のようにデータを持たない列挙型バリアントについては、それ以上値を分解することはできません。Message::Quit というリテラル値そのものにマッチさせることしかできず、そのパターンには変数はありません。
Message::Move のような構造体風の列挙型バリアントでは、構造体にマッチさせるときに指定するのと似たパターンを使えます。バリアント名の後に波かっこを置き、そこでフィールドを変数とともに列挙することで、各部分を分解し、このアームのコード内で使えるようにします。ここでは、リスト 19-13 で行ったのと同じく省略記法を使っています。
Message::Write のように 1 要素のタプルを保持するものや、Message::ChangeColor のように 3 要素のタプルを保持するものといったタプル風の列挙型バリアントでは、パターンはタプルにマッチさせるときに指定するパターンに似ています。パターン内の変数の数は、マッチ対象のバリアントの要素数と一致していなければなりません。
ネストした構造体と列挙型
これまでの例では、構造体または列挙型を 1 段階だけ分解してマッチさせてきましたが、マッチングはネストした項目にも使えます。たとえば、リスト 19-15 のコードをリファクタリングして、ChangeColor メッセージで RGB と HSV の色をサポートできるようにしたものが、リスト 19-16 です。
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
match 式の最初のアームのパターンは、Color::Rgb バリアントを含む Message::ChangeColor 列挙型バリアントにマッチします。そして、そのパターンは内側の 3 つの i32 値に束縛します。2 番目のアームのパターンも Message::ChangeColor 列挙型バリアントにマッチしますが、内側の列挙型は代わりに Color::Hsv にマッチします。2 つの列挙型が関わっていても、こうした複雑な条件を 1 つの match 式で指定できます。
構造体とタプル
分解パターンは、さらに複雑な形で組み合わせたり、入れ子にしたりできます。次の例では、タプルの中に構造体とタプルをネストし、すべてのプリミティブ値を取り出す複雑な分解を示しています。
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
このコードにより、複雑な型をその構成要素に分解して、関心のある値を個別に使えるようになります。
パターンによる分解は、構造体の各フィールドの値のような、値の一部を互いに分けて利用するための便利な方法です。
パターン内で値を無視する
これまで見てきたように、パターン内では値を無視することが役立つ場合があります。たとえば、match の最後のアームでは、実際には何もしないものの、残っているすべての可能な値を考慮する包括的なパターンを得るために、値を無視できます。パターンの中で値全体または値の一部を無視する方法はいくつかあります。_ パターンを使う方法(これはすでに見ました)、別のパターンの中で _ パターンを使う方法、アンダースコアで始まる名前を使う方法、そして .. を使って値の残りの部分を無視する方法です。これらの各パターンをどのように、そしてなぜ使うのかを見ていきましょう。
_ による値全体の無視
アンダースコアは、どんな値にもマッチするが、その値には束縛しないワイルドカードパターンとして使ってきました。これは match 式の最後のアームで特に便利ですが、リスト 19-17 に示すように、関数の引数を含むあらゆるパターンで使えます。
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
このコードは、最初の引数として渡された値 3 を完全に無視し、This code only uses the y parameter: 4 を出力します。
ほとんどの場合、特定の関数引数が不要になったなら、その未使用の引数を含まないようにシグネチャを変更するでしょう。関数引数を無視することは、たとえばトレイトを実装していて、必要な型シグネチャはあるものの、実装内の関数本体では引数の 1 つが不要である場合に特に便利です。こうすると、名前を使った場合に出る未使用の関数引数に関するコンパイラ警告を避けられます。
ネストした _ による値の一部の無視
別のパターンの内部でも _ を使って、値の一部だけを無視することができます。たとえば、値の一部だけをテストしたい一方で、実行したい対応するコードではそのほかの部分を使わない場合です。リスト 19-18 は、設定の値を管理する役割を持つコードを示しています。ビジネス要件としては、ユーザーは設定に対する既存のカスタマイズを上書きしてはならない一方で、その設定が現在未設定であれば、設定を解除したり値を与えたりできるべきです。
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
このコードは Can't overwrite an existing customized value を出力し、その後
setting is Some(5) を出力します。最初の match アームでは、どちらの Some
バリアントの内部の値についてもマッチしたり使ったりする必要はありませんが、
setting_value と new_setting_value が Some バリアントである場合はテストする必要があります。その場合、setting_value を変更しない理由を出力し、実際に変更もされません。
それ以外のすべての場合(setting_value または new_setting_value のいずれかが None の場合)では、2 番目のアームの _ パターンで表されているように、new_setting_value が setting_value になることを許可したいのです。
また、1 つのパターン内の複数の場所でアンダースコアを使って、特定の値を無視することもできます。リスト 19-19 は、5 要素のタプルのうち 2 番目と 4 番目の値を無視する例を示しています。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
このコードは Some numbers: 2, 8, 32 を出力し、4 と 16 の値は無視されます。
名前を _ で始めることによる未使用変数
変数を作成してもどこでも使わない場合、Rust は通常、未使用の変数がバグである可能性があるため警告を出します。しかし、プロトタイプを作っているときやプロジェクトを始めたばかりのときのように、まだ使わない変数を作成できると便利なことがあります。このような状況では、変数名をアンダースコアで始めることで、その未使用変数について警告しないよう Rust に伝えることができます。リスト 19-20 では、2 つの未使用変数を作成していますが、このコードをコンパイルすると、そのうち 1 つについてだけ警告が出るはずです。
fn main() {
let _x = 5;
let y = 10;
}
ここでは、変数 y を使っていないことについては警告が出ますが、_x を使っていないことについては警告が出ません。
_ だけを使う場合と、アンダースコアで始まる名前を使う場合のあいだには、微妙な違いがあることに注意してください。構文 _x は依然として値をその変数に束縛しますが、_ はまったく束縛しません。この違いが重要になるケースを示すために、リスト 19-21 ではエラーが発生します。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
_s に s の値が依然としてムーブされるため、s を再び使えなくなり、エラーを受け取ることになります。しかし、アンダースコア単体を使う場合は、値に束縛されることはまったくありません。リスト 19-22 は、s が _ にムーブされないため、エラーなしでコンパイルされます。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
このコードがまったく問題なく動作するのは、s を何にも束縛していないからです。ムーブもされません。
.. による値の残りの部分
多くの部分を持つ値では、.. 構文を使って特定の部分を使い、残りを無視できます。これにより、無視する値ごとにアンダースコアを列挙する必要がなくなります。.. パターンは、そのパターンの残りの部分で明示的にマッチしていない値の部分をすべて無視します。リスト 19-23 では、3 次元空間内の座標を保持する Point 構造体があります。match 式では、x 座標に対してのみ処理を行い、y および z フィールドの値は無視したいと考えています。
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
x の値を列挙し、そのあとに .. パターンを含めるだけです。これは、特にフィールドの多い構造体を扱っていて、そのうち 1 つか 2 つのフィールドだけが関係する状況では、y: _ や z: _ を列挙するよりも手早く書けます。
構文 .. は、必要な数の値に展開されます。リスト 19-24 は、タプルで .. を使う方法を示しています。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
このコードでは、最初と最後の値が first と last にマッチします。.. はその間にあるすべてをマッチさせて無視します。
ただし、.. の使用は曖昧であってはなりません。どの値をマッチさせ、どの値を無視する意図なのかが不明確な場合、Rust はエラーを出します。リスト 19-25 は、.. を曖昧に使っている例であるため、コンパイルできません。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
この例をコンパイルすると、次のエラーが得られます。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust には、second で値をマッチさせる前にタプル内のいくつの値を無視し、その後さらにいくつの値を無視するのかを判断することができません。このコードは、2 を無視して second を 4 に束縛し、その後 8、16、32 を無視したいという意味かもしれませんし、2 と 4 を無視して second を 8 に束縛し、その後 16 と 32 を無視したいという意味かもしれません。ほかにもさまざまな解釈がありえます。変数名 second には Rust にとって特別な意味はないため、このように 2 か所で .. を使うと曖昧になり、コンパイラエラーになります。
マッチガードで条件式を追加する
マッチガード は、match アーム内でパターンの後に指定する追加の if 条件であり、そのアームが選ばれるためにはその条件も満たされなければなりません。マッチガードは、パターンだけでは表現できないより複雑な考えを表すのに役立ちます。ただし、利用できるのは match 式だけであり、if let 式や while let 式では使えません。
この条件では、パターン内で作成された変数を使えます。リスト19-26は、最初のアームが Some(x) というパターンを持ち、さらに if x % 2 == 0 というマッチガードも持つ match を示しています(この条件は数値が偶数なら true になります)。
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
この例は The number 4 is even を出力します。num が最初のアームのパターンと比較されると、Some(4) は Some(x) にマッチするので一致します。次に、マッチガードが x を 2 で割った余りが 0 に等しいかどうかを確認し、実際にそうなので、最初のアームが選択されます。
もし num が Some(5) だった場合、最初のアームのマッチガードは false になっていたでしょう。というのも、5 を 2 で割った余りは 1 であり、0 には等しくないからです。すると Rust は2つ目のアームに進みます。2つ目のアームにはマッチガードがなく、したがって任意の Some バリアントにマッチするので、こちらが一致します。
if x % 2 == 0 という条件をパターンの中で表現する方法はないため、マッチガードによってこのロジックを表現できるようになります。この表現力が増すことの欠点は、マッチガード式が関わる場合、コンパイラが網羅性を検査しようとしないことです。
リスト19-11を説明したとき、パターンのシャドーイング問題を解決するためにマッチガードを使えると述べました。match の外側にある変数を使う代わりに、match 式内のパターンの中で新しい変数を作成したことを思い出してください。その新しい変数のため、外側の変数の値に対してテストできませんでした。リスト19-27は、この問題を修正するためにマッチガードをどう使えるかを示しています。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
このコードは今度は Default case, x = Some(5) を出力します。2つ目の match アームのパターンでは、外側の y をシャドーイングする新しい変数 y は導入していないため、マッチガードの中で外側の y を使えます。外側の y をシャドーイングしてしまう Some(y) というパターンを指定する代わりに、Some(n) を指定します。これにより、新しい変数 n が作られますが、match の外側には n という変数がないので、何もシャドーイングしません。
マッチガード if n == y はパターンではないので、新しい変数は導入しません。この y は、それをシャドーイングする新しい y ではなく、まさに 外側の y です。そして、n と y を比較することで、外側の y と同じ値を持つ値を探せます。
マッチガードでは or 演算子 | を使って複数のパターンを指定することもできます。マッチガードの条件は、それらすべてのパターンに適用されます。リスト19-28は、| を使うパターンとマッチガードを組み合わせたときの優先順位を示しています。この例で重要なのは、if y というマッチガードが 4、5、そして 6 に適用されることであり、一見すると if y が 6 にしか適用されないように見える点です。
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
このマッチ条件は、x の値が 4、5、または 6 に等しく、かつ y が true の場合にのみ、そのアームが一致することを表しています。このコードを実行すると、最初のアームのパターンは x が 4 なので一致しますが、マッチガード if y は false なので、最初のアームは選ばれません。コードは次のアームに進み、そちらは一致するため、このプログラムは no を出力します。その理由は、if 条件が最後の値 6 だけではなく、パターン全体 4 | 5 | 6 に適用されるからです。言い換えると、パターンに対するマッチガードの優先順位は次のように振る舞います。
(4 | 5 | 6) if y => ...
次のようになるのではありません。
4 | 5 | (6 if y) => ...
コードを実行すると、この優先順位の振る舞いは明らかです。もしマッチガードが | 演算子を使って指定された値の一覧の最後の値にしか適用されないのであれば、そのアームは一致し、プログラムは yes を出力していたでしょう。
@ バインディングを使う
at 演算子 @ を使うと、その値がパターンマッチするかどうかをテストすると同時に、その値を保持する変数を作成できます。リスト19-29では、Message::Hello の id フィールドが 3..=7 の範囲内にあることをテストしたいと考えています。また、その値を変数 id に束縛して、そのアームに対応するコードの中で使えるようにもしたいと考えています。
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
この例は Found an id in range: 5 を出力します。範囲 3..=7 の前に id @ を指定することで、その範囲にマッチした値が何であっても、それを id という名前の変数に束縛すると同時に、その値が範囲パターンにマッチしたこともテストしています。
2つ目のアームでは、パターン内に範囲しか指定していないため、そのアームに対応するコードには id フィールドの実際の値を含む変数がありません。id フィールドの値は 10、11、あるいは 12 だったかもしれませんが、そのパターンに対応するコードはどれなのかを知りません。id の値を変数に保存していないため、パターンのコードは id フィールドの値を使えません。
最後のアームでは、範囲なしで変数を指定しているため、id という名前の変数として、そのアームのコードで使える値があります。これは、構造体フィールドの省略記法を使っているからです。しかしこのアームでは、最初の2つのアームで行ったように、id フィールドの値に対して何のテストも適用していません。どんな値でもこのパターンにマッチします。
@ を使うと、1つのパターンの中で値をテストし、その値を変数に保存できます。
まとめ
Rust のパターンは、異なる種類のデータを区別するのに非常に役立ちます。match 式で使うと、Rust はパターンが取り得るすべての値を網羅していることを保証し、そうでなければプログラムはコンパイルされません。let 文や関数パラメータにおけるパターンは、そうした構文をより便利にし、値をより小さな部分に分解して、それらの部分を変数に代入できるようにします。必要に応じて、単純なパターンも複雑なパターンも作成できます。
次は、本書の最後から2番目の章として、Rust のさまざまな機能の高度な側面を見ていきます。