match制御フロー構造
Rustには、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれるmatchと呼ばれる、
非常に強力な制御フロー構造があります。パターンは、リテラル値、変数名、ワイルドカードやその他多数のもので構成することができます;
第18章で、全ての種類のパターンと、その目的については解説します。matchのパワーは、
パターンの表現力とコンパイラが全てのありうるパターンを処理しているかを確認してくれるという事実に由来します。
match式をコイン並べ替え装置のようなものと考えてください: コインは、様々なサイズの穴が空いた通路を流れ落ち、
各コインは、サイズのあった最初の穴に落ちます。同様に、値はmatchの各パターンを通り抜け、値が「適合する」最初のパターンで、
値は紐付けられたコードブロックに落ち、実行中に使用されるわけです。
せっかくコインについて話したので、それをmatchを使用する例にとってみましょう!数え上げ装置と同じ要領で未知のアメリカコインを一枚取り、
どの種類のコインなのか決定し、その価値をセントで返す関数をリスト6-3で示したように記述することができます。
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
リスト6-3: enumとそのenumの列挙子をパターンにしたmatch式
value_in_cents関数内のmatchを噛み砕きましょう。まず、matchキーワードに続けて式を並べています。
この式は今回の場合、値coinです。ifで使用した条件式と非常に酷似しているみたいですね。しかし、大きな違いがあります:
ifでは、条件は論理値に評価される必要がありますが、ここでは、どんな型でも構いません。この例におけるcoinの型は、
1行目で定義したCoin enumです。
次は、matchアームです。一本のアームには2つの部品があります: パターンと何らかのコードです。
今回の最初のアームはCoin::Pennyという値のパターンであり、パターンと動作するコードを区別する=>演算子が続きます。
この場合のコードは、ただの値1です。各アームは次のアームとカンマで区切られています。
このmatch式が実行されると、結果の値を各アームのパターンと順番に比較します。パターンに値がマッチしたら、
そのコードに紐付けられたコードが実行されます。パターンが値にマッチしなければ、コイン並べ替え装置と全く同じように、
次のアームが継続して実行されます。必要なだけパターンは存在できます: リスト6-3では、matchには4本のアームがあります。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果がmatch式全体の戻り値になります。
典型的に、アームのコードが短い場合、波かっこは使用しません。リスト6-3では、各アームが値を返すだけなので、
これに倣っています。マッチのアームで複数行のコードを走らせたいのなら、波かっこを使わなくてはなりませんが、
この場合アームの後のカンマは省略することができます。
例えば、以下のコードは、メソッドがCoin::Pennyとともに呼び出されるたびに「Lucky penny!」と表示しつつ、
ブロックの最後の値、1を返します。
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
値に束縛されるパターン
マッチのアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。こうして、 enumの列挙子から値を取り出すことができます。
例として、enumの列挙子の一つを中にデータを保持するように変えましょう。1999年から2008年まで、
アメリカは、片側に50の州それぞれで異なるデザインをしたクォーターコインを鋳造していました。
他のコインは州のデザインがなされることはなかったので、クォーターだけがこのおまけの値を保持します。
Quarter列挙子を変更して、UsState値が中に保持されるようにすることでenumにこの情報を追加でき、
それをしたのがリスト6-4のコードになります。
#[derive(Debug)] // すぐに州を検査できるように enum UsState { Alabama, Alaska, // --略-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() {}
リスト6-4: Quarter列挙子がUsStateの値も保持するCoin enum
友人の一人が50州全部のクォーターコインを収集しようとしているところを想像しましょう。コインの種類で小銭を並べ替えつつ、 友人が持っていない種類だったら、コレクションに追加できるように、各クォーターに関連した州の名前を出力します。
このコードのmatch式では、Coin::Quarter列挙子の値にマッチするstateという名の変数をパターンに追加します。
Coin::Quarterがマッチすると、state変数はそのクォーターのstateの値に束縛されます。それから、
stateをそのアームのコードで使用できます。以下のようにですね:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } 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 } } } fn main() { value_in_cents(Coin::Quarter(UsState::Alaska)); }
value_in_cents(Coin::Quarter(UsState::Alaska))と呼び出すつもりだったなら、coinは
Coin::Quarter(UsState::Alaska)になります。その値をmatchの各アームと比較すると、
Coin::Quarter(state)に到達するまで、どれにもマッチしません。その時に、stateに束縛されるのは、
UsState::Alaskaという値です。そして、println!式でその束縛を使用することができ、
そのため、Coin enumの列挙子からQuarterに対する中身のstateの値を取得できたわけです。
Option<T>とのマッチ
前節では、Option<T>を使用する際に、Someケースから中身のTの値を取得したくなりました。要するに、
Coin enumに対して行ったように、matchを使ってOption<T>を扱うこともできるというわけです!
コインを比較する代わりに、Option<T>の列挙子を比較するのですが、match式の動作の仕方は同じままです。
Option<i32>を取る関数を書きたくなったとし、中に値があったら、その値に1を足すことにしましょう。
中に値がなければ、関数はNone値を返し、何も処理を試みるべきではありません。
matchのおかげで、この関数は大変書きやすく、リスト6-5のような見た目になります。
fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
リスト6-5: Option<i32>にmatch式を使う関数
plus_oneの最初の実行についてもっと詳しく検証しましょう。plus_one(five)と呼び出した時、
plus_oneの本体の変数xはSome(5)になります。そして、これをマッチの各アームと比較します:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)という値は、Noneというパターンにはマッチしませんので、次のアームに処理が移ります:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)はSome(i)にマッチしますか?しますね!列挙子が同じです。iはSomeに含まれる値に束縛されるので、
iは値5になります。それから、このマッチのアームのコードが実行されるので、iの値に1を足し、
合計の6を中身にした新しいSome値を生成します。
さて、xがNoneになるリスト6-5の2回目のplus_oneの呼び出しを考えましょう。matchに入り、
最初のアームと比較します:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
マッチします!足し算する値がないので、プログラムは停止し、=>の右辺にあるNone値が返ります。
最初のアームがマッチしたため、他のアームは比較されません。
matchとenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう:
enumに対しmatchし、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、
一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。
マッチは包括的
もう一つ議論する必要のあるmatchの観点があります: アームのパターンはすべての可能性を網羅しなくてはなりません。
こんなバージョンのplus_one関数を考えてください、これにはバグがありコンパイルできないでしょう:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Noneの場合を扱っていないため、このコードはバグを生みます。幸い、コンパイラが捕捉できるバグです。
このコードのコンパイルを試みると、こんなエラーが出ます:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`が網羅されていません)
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
(パターン`None`が網羅されていません)
|
note: `Option<i32>` defined here
--> /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/option.rs:570:1
::: /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/option.rs:574:5
|
= note: not covered
= 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!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
全可能性を網羅していないことをコンパイラは検知しています。もっと言えば、どのパターンを忘れているかさえ知っているのです。
Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。
特にOption<T>の場合には、私達が明示的にNoneの場合を処理するのを忘れないようにしてくれます。
nullになるかもしれないのに値があると思い込まないよう、すなわち前に議論した10億ドルの失敗を犯すことができないよう、
コンパイラが保護してくれるわけです。
catch-allパターンとプレースホルダー(_)
enumを使用することで、いくつかの特定の値に対して特別な操作を行うが、他のすべての値に対してはデフォルトの操作を行う、ということができます。
ゲームを実装しているところを想像してください。
サイコロを振って3の目が出たら、そのプレイヤーは移動できませんが、代わりにおしゃれな帽子をもらえます。
サイコロを振って7の目が出たら、そのプレイヤーはおしゃれな帽子を失います。
他のすべての値については、そのプレイヤーはゲーム盤上で同じ数だけマスを移動します。
以下はこのロジックを実装するmatchです。
ただしサイコロを振った結果はランダム値ではなくハードコードされており、また他のすべてのロジックは、それを実際に実装するのは本題ではないので、本体の無い関数によって表現されています:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} }
最初の2つのアームについては、パターンはリテラル値3および7です。
他のあらゆる可能な値を網羅する最後のアームについては、パターンは変数で、この変数にはotherと名付けることを選びました。
otherアームで実行されるコードは、move_player関数にこの変数を渡すことで、この変数を使用しています。
u8が取りうるすべての値を列挙していないにも関わらず、このコードはコンパイルできます。
最後のパターンが、個別に列挙していないすべての値にマッチするからです。
このcatch-allパターンのおかげで、matchは包括的でなくてはならないという必要条件が満たされます。
パターンは順に評価されるので、catch-allアームは最後に書く必要があることに注意してください。
catch-allアームを先に書いてしまうと他のアームは絶対に実行されなくなってしまうため、
catch-allの後にアームを追加するとコンパイラが警告を発するでしょう!
Rustには、catch-allしたいが、catch-allパターン内で値を使用したくない時に使用できるパターンもあります:
_は任意の値にマッチし、その値を束縛しない特別なパターンです。
これはコンパイラにその値を使用しないということを伝えるので、コンパイラは未使用の変数についての警告を発しなくなるしょう。
ゲームのルールを変更しましょう: これからは、3または7以外の目を出したら、もう一度サイコロを振らなくてはなりません。
catch-allの値を使用する必要がなくなるので、other変数の代わりに_を使用するようにコードを変更します:
fn main() { 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以外の目を出したら、プレイヤーの番には何も起きません。
以下の_アームのコードのように、ユニット値(「タプル型」節で説明した空タプル型)を使用することでこれを表現できます:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} }
このコードでは、先の方のアームのパターンにマッチしないあらゆる値は使用せず、 この場合にはいかなるコードも実行したくないということを、コンパイラに明示的に伝えています。
パターンとマッチングについては第18章でさらに深く取り扱います。
ひとまず、match式ではちょっと長ったらしいという状況で便利かもしれない、if let構文に進むことにましょう。