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))と呼び出すつもりだったなら、coinCoin::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の本体の変数xSome(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)にマッチしますか?しますね!列挙子が同じです。iSomeに含まれる値に束縛されるので、 iは値5になります。それから、このマッチのアームのコードが実行されるので、iの値に1を足し、 合計の6を中身にした新しいSome値を生成します。

さて、xNoneになるリスト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構文に進むことにましょう。