Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

match 制御フロー構文

Rust には match という非常に強力な制御フロー構文があり、これを使うと値を一連のパターンと比較し、どのパターンに一致したかに基づいてコードを実行できます。パターンは、リテラル値、変数名、ワイルドカード、その他さまざまな要素で構成できます。第19章 では、あらゆる種類のパターンとその働きを扱います。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() {}

value_in_cents 関数内の match を見ていきましょう。まず、match キーワードの後に式を書きます。この場合は値 coin です。これは if で使う条件式とよく似ているように見えますが、大きな違いがあります。if では条件は真偽値に評価される必要がありますが、ここではどんな型でもかまいません。この例での coin の型は、1行目で定義した Coin enum です。

次に match アームがあります。アームは、パターンと何らかのコードという2つの部分から成ります。ここでの最初のアームは、値 Coin::Penny というパターンと、それに続く、パターンと実行するコードを区切る => 演算子を持っています。この場合のコードは単に値 1 です。各アームはカンマで区切られます。

match 式が実行されると、その式の評価結果の値が各アームのパターンと順番に比較されます。あるパターンが値に一致した場合、そのパターンに関連付けられたコードが実行されます。そのパターンが値に一致しない場合、コイン仕分け機と同じように、実行は次のアームへ進みます。必要なだけアームを持つことができます。リスト6-3では、match は4つのアームを持っています。

各アームに関連付けられたコードは式であり、一致したアームの式の評価結果が match 式全体の返り値になります。

match アームのコードが短い場合は、通常、波かっこを使いません。リスト6-3では各アームが単に値を返すだけなので、そうなっています。match アームで複数行のコードを実行したい場合は、波かっこを使わなければなりません。その場合、アームの後ろのカンマは省略可能です。たとえば、次のコードは、このメソッドが 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() {}

値に束縛するパターン

match アームのもう1つの便利な機能は、パターンに一致した値の一部を束縛できることです。これにより、enum バリアントから値を取り出せます。

例として、enum のバリアントの1つが内部にデータを保持するように変更してみましょう。1999年から2008年にかけて、米国は50州それぞれについて片面のデザインが異なるクォーター硬貨を鋳造しました。州ごとのデザインが施されたのはほかの硬貨にはなかったため、この追加の値を持つのはクォーター硬貨だけです。Quarter バリアントが内部に保持する UsState 値を含むように変更することで、この情報を enum に追加できます。リスト6-4ではそのようにしています。

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

友人が50州すべてのクォーター硬貨を集めようとしていると想像してみてください。ばらばらの小銭を硬貨の種類ごとに仕分けしながら、各クォーターに対応する州の名前も読み上げることにしましょう。そうすれば、それが友人のまだ持っていないものなら、そのコレクションに加えられます。

このコードの match 式では、Coin::Quarter バリアントの値に一致するパターンに state という変数を追加します。Coin::Quarter に一致すると、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 バリアントから内部の州の値を取り出せます。

Option<T>match パターン

前の節では、Option<T> を使うときに Some の場合から内部の T 値を取り出したいと考えました。Coin enum で行ったのと同じように、Option<T>match を使って扱えます! 硬貨を比較する代わりに 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);
}

plus_one の最初の実行をもう少し詳しく見てみましょう。plus_one(five) を呼び出すと、plus_one 本体の変数 xSome(5) という値を持ちます。次に、それを各 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);
}

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 を取ります。すると match アーム内のコードが実行され、i の値に 1 を足して、合計 6 を中に入れた新しい Some 値を作成します。

次に、リスト 6-5 にある plus_one の 2 回目の呼び出しを考えてみましょう。このとき xNone です。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 は網羅的である

match には、もう 1 つ説明しておくべき側面があります。アームのパターンは、すべての可能性を網羅していなければなりません。バグがあり、コンパイルできない次の 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 のケースを処理していないため、このコードはバグを引き起こします。幸いなことに、これは Rust が検出できるバグです。このコードをコンパイルしようとすると、次のようなエラーが表示されます。

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
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
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597: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 は、すべての可能なケースを網羅していないことを理解しており、しかも、どのパターンを忘れたのかまで把握しています!Rust の match は 網羅的 です。コードが有効であるためには、ありうるすべての可能性を余すところなく扱わなければなりません。特に Option<T> の場合、Rust が None のケースを明示的に処理し忘れないようにしてくれることで、実際には null かもしれないのに値があると思い込むことから私たちを守り、先に述べた 10 億ドルの過ちを不可能にしてくれます。

キャッチオールパターンと _ プレースホルダー

enum を使うと、いくつかの特定の値に対しては特別な動作をさせ、それ以外のすべての値に対しては 1 つのデフォルト動作をさせることもできます。たとえば、ゲームを実装していると想像してみてください。サイコロの出目が 3 なら、プレイヤーは移動せず、代わりにおしゃれな新しい帽子を手に入れます。7 が出たら、プレイヤーはおしゃれな帽子を 1 つ失います。それ以外の値なら、プレイヤーはゲーム盤の上をその数だけ進みます。次の 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 つのアームでは、パターンはリテラル値 37 です。その他のすべての値をカバーする最後のアームでは、パターンは other という名前を付けた変数です。other アームで実行されるコードは、その変数を move_player 関数に渡して使います。

このコードは、u8 が取りうるすべての値を列挙していないにもかかわらず、コンパイルできます。なぜなら、最後のパターンが、個別に列挙されていないすべての値にマッチするからです。このキャッチオールパターンは、match が網羅的でなければならないという要件を満たしています。パターンは順番に評価されるため、キャッチオールアームは最後に置かなければならないことに注意してください。もしキャッチオールアームをもっと前に置いてしまうと、他のアームは決して実行されないため、キャッチオールの後にアームを追加すると Rust は警告します!

Rust には、キャッチオールは欲しいけれど、そのキャッチオールパターンの値を 使い たくないときに使えるパターンもあります。_ は任意の値にマッチし、その値に束縛しない特別なパターンです。これは Rust に、その値を使わないことを伝えるので、Rust は未使用変数について警告しません。

ゲームのルールを変更してみましょう。今度は、3 または 7 以外が出たら、もう一度振らなければなりません。キャッチオールの値を使う必要がなくなったので、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() {}
}

ここでは、先行するアームのパターンにマッチしない他のどの値も使わず、この場合にはどんなコードも実行したくないということを Rust に明示的に伝えています。

パターンとマッチングについては、第19章 でさらに詳しく扱います。ここでは、match 式が少し冗長になる状況で役立つ if let 構文へ進みましょう。