はじめに
マッチ制御フロー構文へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、Rust における強力なmatch制御フロー構文を調べます。これにより、パターンマッチングと一致したパターンに基づくコードの実行が可能になります。
マッチ制御フロー構文へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、Rust における強力なmatch制御フロー構文を調べます。これにより、パターンマッチングと一致したパターンに基づくコードの実行が可能になります。
Rust には、「マッチ」と呼ばれる非常に強力な制御フロー構文があり、これを使うと値を一連のパターンと比較し、一致したパターンに基づいてコードを実行できます。パターンはリテラル値、変数名、ワイルドカードなどで構成でき、18 章ではすべての種類のパターンとその機能について説明します。「マッチ」の力は、パターンの表現力と、コンパイラがすべての可能なケースを処理することを確認するという点にあります。
「マッチ」式をコイン分類機に例えると、コインは様々な大きさの穴がある軌道を伝って流れ、それぞれのコインは最初に合致する穴を通り抜けます。同じように、値は「マッチ」内の各パターンを通り、最初に値が「合致」するパターンで、その値は実行中に使用される関連するコードブロックに入ります。
コインについて言えば、「マッチ」を使って例にしましょう!未知のアメリカの硬貨を受け取り、カウント機と同じように、それがどの硬貨であるかを判断し、セントでの価値を返す関数を書けます。例を以下に示します。
1 enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
2 match coin {
3 Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
リスト 6-3:列挙型と、列挙型のバリアントをパターンとする「マッチ」式
value_in_cents関数内の「マッチ」を分解してみましょう。まず、「マッチ」キーワードの後に式を列挙します。この場合、それは値coinです[2]。これはifで使用される式に非常に似ていますが、大きな違いがあります。ifの場合、式はブール値を返す必要がありますが、ここでは任意の型を返すことができます。この例でのcoinの型は、[1]で定義したCoin列挙型です。
次に「マッチ」のアームです。アームには 2 つの部分があります。パターンとコードです。ここで最初のアームは、パターンが値Coin::Pennyで、その後にパターンと実行するコードを区切る=>演算子です[3]。この場合のコードはただの値1です。各アームはコンマで区切られています。
「マッチ」式が実行されると、結果の値が順に各アームのパターンと比較されます。パターンが値と一致すると、そのパターンに関連付けられたコードが実行されます。そのパターンが値と一致しない場合、実行は次のアームに続きます。コイン分類機と同じです。必要なだけのアームを持つことができます。リスト 6-3 では、「マッチ」には 4 つのアームがあります。
各アームに関連付けられたコードは式であり、一致するアーム内の式の結果の値は、全体の「マッチ」式に戻される値です。
アームのコードが短い場合、通常は波括弧を使いません。リスト 6-3 のように、各アームがただの値を返す場合です。マッチのアームで複数行のコードを実行したい場合は、波括弧を使う必要があり、その後のコンマは省略可能です。たとえば、次のコードは、メソッドがCoin::Pennyで呼び出されるたびに「幸運な 1 セント硬貨!」と表示しますが、ブロックの最後の値1を返します。
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
マッチのアームのもう一つの便利な機能は、パターンに一致する値の一部にバインドできることです。これが、列挙型のバリアントから値を抽出する方法です。
例として、列挙型のバリアントの一つを変更して、その中にデータを保持させましょう。1999 年から 2008 年まで、アメリカは 50 州それぞれに異なるデザインの 1 セント硬貨を発行しました。他の硬貨には州のデザインはありませんので、1 セント硬貨だけがこの追加の値を持っています。この情報をenumに追加するには、Quarterバリアントを変更して、その中に格納されたUsState値を含めます。これをリスト 6-4 で行っています。
#[derive(Debug)] // これですぐに状態を調べることができます
enum UsState {
Alabama,
Alaska,
--snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
リスト 6-4:QuarterバリアントがUsState値も保持するCoin列挙型
友人が 50 州の 1 セント硬貨をすべて集めようとしていると想像してみましょう。私たちが硬貨の種類で零銭を分類する間、各 1 セント硬貨に関連付けられた州の名前も呼び出します。もし友人が持っていない硬貨があれば、彼らはそれをコレクションに追加できるようにします。
このコードのマッチ式では、Coin::Quarterの値と一致するパターンにstateという変数を追加します。Coin::Quarterが一致すると、state変数はその 1 セント硬貨の州の値にバインドされます。そして、そのアームのコードでstateを使うことができます。例えばこのように:
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
value_in_cents(Coin::Quarter(UsState::Alaska))を呼び出した場合、coinはCoin::Quarter(UsState::Alaska)になります。その値を各マッチのアームと比較すると、Coin::Quarter(state)に到達するまでは一致しません。その時点で、stateのバインドは値UsState::Alaskaになります。そして、そのバインドをprintln!式で使うことができるので、QuarterのCoin列挙型のバリアントから内部の州の値を取り出すことができます。
Option<T> とのマッチング前節では、Option<T> を使用する際に Some の場合の内部の T 値を取り出したかったのですが、Coin 列挙型と同じように、match を使って Option<T> を処理することもできます!コインを比較する代わりに、Option<T> のバリアントを比較しますが、match 式の動作方法は同じです。
Option<i32> を受け取り、内部に値があればその値に 1 を加える関数を書きたいとしましょう。内部に値がなければ、関数は None 値を返し、何らかの操作を行おうとしないようにします。
この関数は match のおかげで非常に簡単に書けます。リスト 6-5 のようになります。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
1 None => None,
2 Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4
リスト 6-5:Option<i32> で match 式を使用する関数
plus_one の最初の実行をもう少し詳細に見てみましょう。plus_one(five) [3] を呼び出すと、plus_one の本体の変数 x には Some(5) の値が入ります。そして、それを各マッチのアームと比較します。
None => None,
Some(5) の値はパターン None [1] と一致しませんので、次のアームに進みます。
Some(i) => Some(i + 1),
Some(5) は Some(i) [2] と一致しますか?そうです、一致します!同じバリアントです。i は Some に含まれる値にバインドされるので、i には値 5 が入ります。そして、マッチのアームのコードが実行されるので、i の値に 1 を加えて、合計 6 を含む新しい Some 値を作成します。
次に、リスト 6-5 の plus_one の 2 番目の呼び出しを考えてみましょう。この場合、x は None [4] です。match に入り、最初のアーム [1] と比較します。
一致します!加える値がないので、プログラムは停止して、=> の右辺の None 値を返します。最初のアームが一致したので、他のアームは比較されません。
match と列挙型を組み合わせると、多くの状況で役立ちます。Rust のコードではこのパターンがよく見られます。列挙型に対して match し、内部のデータに変数をバインドし、それに基づいてコードを実行します。最初は少し慣れにくいかもしれませんが、慣れると、すべての言語にこれがあったらいいなと思うようになります。これは一貫してユーザーに人気があります。
match について議論する必要があるもう一つの点があります。アームのパターンはすべての可能性を網羅しなければなりません。plus_one 関数のこのバージョンを見てみましょう。これにはバグがあり、コンパイルされません。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
None のケースを処理していないので、このコードはバグを引き起こします。幸いなことに、これは Rust がキャッチする方法を知っているバグです。このコードをコンパイルしようとすると、このエラーが表示されます。
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding
a match arm with a wildcard pattern or an explicit pattern as
shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
Rust は、私たちがすべての可能なケースを網羅していないことを知っており、忘れたパターンさえ知っています!Rust のマッチは網羅的です。コードが有効になるためには、最後の可能性まですべて網羅しなければなりません。特に Option<T> の場合、Rust が明示的に None のケースを処理することを忘れないように防いでくれるので、値が null の場合に値があると仮定することから免れ、前述の 10 億ドルのミスを引き起こすことができなくなります。
列挙型を使うことで、いくつかの特定の値に対して特別なアクションを実行することもできますが、それ以外のすべての値に対しては 1 つのデフォルトのアクションを実行します。あるゲームを実装していると想像してみましょう。サイコロを振って出た目が 3 の場合、プレイヤーは動かず、代わりに新しい素敵な帽子を手に入れます。出た目が 7 の場合、プレイヤーは素敵な帽子を失います。それ以外のすべての値の場合、プレイヤーはゲーム盤上をその数だけ移動します。ここにそのロジックを実装したmatchがあります。サイコロの目はランダムな値ではなくハードコードされており、この例の範囲外なので、他のロジックは本体のない関数で表されています。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
1 other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
最初の 2 つのアームでは、パターンはリテラル値の3と7です。他のすべての可能な値をカバーする最後のアームでは、パターンはotherと名付けた変数です[1]。otherのアームで実行されるコードは、その変数をmove_player関数に渡すことで使用します。
このコードはコンパイルされます。u8が持り得るすべての可能な値を列挙していなくてもです。なぜなら、最後のパターンが明示的に列挙されていないすべての値に一致するからです。この全てをキャッチするパターンは、matchが網羅的である必要があるという要件を満たしています。全てをキャッチするアームは最後に置かなければならないことに注意してください。なぜなら、パターンは順に評価されるからです。全てをキャッチするアームをより前に置くと、他のアームは決して実行されなくなります。したがって、全てをキャッチするアームの後にアームを追加すると、Rust が警告します!
Rust には、全てをキャッチする必要があるが、全てをキャッチするパターンの値を使わない場合に使用できるパターンもあります。_は特別なパターンで、どんな値にも一致し、その値にバインドされません。これは、私たちがその値を使わないことを Rust に伝えるもので、そのため Rust は未使用の変数に関する警告を出さなくなります。
ゲームのルールを変更しましょう。今では、3 または 7 以外の目を出した場合、もう一度振る必要があります。これ以上全てをキャッチする値を使う必要がないので、コードを変更して、otherという変数の代わりに_を使うことができます。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
この例も網羅性の要件を満たしています。なぜなら、最後のアームで明示的に他のすべての値を無視しているからです。何かを忘れているわけではありません。
最後に、ゲームのルールをもう一度変更します。3 または 7 以外の目を出した場合、そのターンでは何も起こらなくなります。これを表現するには、_のアームに付随するコードとしてユニット値(「タプル型」で触れた空のタプル型)を使うことができます。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
ここでは、Rust に明示的に、より前のアームのパターンに一致しない他の値を使わないこと、そしてこの場合には何のコードも実行したくないことを伝えています。
18 章では、パターンとマッチングに関するさらに多くの内容を扱います。今のところ、match式が少々長々しい場合に便利なif let構文に移ります。
おめでとうございます!「マッチによる制御フロー構文」の実験を完了しました。スキルを向上させるために、LabEx でさらに実験を行って練習してください。