matchフロー制御演算子

Rustには、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれるmatchと呼ばれる、 非常に強力なフロー制御演算子があります。パターンは、リテラル値、変数名、ワイルドカードやその他多数のもので構成することができます; 第18章で、全ての種類のパターンと、その目的については解説します。matchのパワーは、 パターンの表現力とコンパイラが全てのありうるパターンを処理しているかを確認してくれるという事実に由来します。

match式をコイン並べ替え装置のようなものと考えてください: コインは、様々なサイズの穴が空いた通路を流れ落ち、 各コインは、サイズのあった最初の穴に落ちます。同様に、値はmatchの各パターンを通り抜け、値が「適合する」最初のパターンで、 値は紐付けられたコードブロックに落ち、実行中に使用されるわけです。

コインについて話したので、それをmatchを使用する例にとってみましょう!数え上げ装置と同じ要領で未知のアメリカコインを一枚取り、 どの種類のコインなのか決定し、その価値をセントで返す関数をリスト6-3で示したように記述することができます。


# #![allow(unused_variables)]
#fn main() {
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
#}

リスト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を返すでしょう。


# #![allow(unused_variables)]
#fn main() {
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter,
# }
#
fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
#}

値に束縛されるパターン

マッチのアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。こうして、 enumの列挙子から値を取り出すことができます。

例として、enumの列挙子の一つを中にデータを保持するように変えましょう。1999年から2008年まで、 アメリカは、片側に50の州それぞれで異なるデザインをしたクォーターコインを鋳造していました。 他のコインは州のデザインがなされることはなかったので、クォーターだけがこのおまけの値を保持します。 Quarter列挙子を変更して、UsState値が中に保持されるようにすることでenumにこの情報を追加でき、 それをしたのがリスト6-4のコードになります。


# #![allow(unused_variables)]
#fn main() {
#[derive(Debug)] // すぐに州を点検できるように
enum UsState {
    Alabama,
    Alaska,
    // ... などなど
}

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

リスト6-4: Quarter列挙子がUsStateの値も保持するCoin enum

友人の一人が50州全部のクォーターコインを収集しようとしているところを想像しましょう。コインの種類で小銭を並べ替えつつ、 友人が持っていない種類だったら、コレクションに追加できるように、各クォーターに関連した州の名前を出力します。

このコードのmatch式では、Coin::Quarter列挙子の値にマッチするstateという名の変数をパターンに追加します。 Coin::Quarterがマッチすると、state変数はそのクォーターのstateの値に束縛されます。それから、 stateをそのアームのコードで使用できます。以下のようにですね:


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug)]
# enum UsState {
#    Alabama,
#    Alaska,
# }
#
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(UsState),
# }
#
fn value_in_cents(coin: Coin) -> u32 {
    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))と呼び出すつもりだったなら、coinCoin::Quarter(UsState::Alaska)になります。その値をmatchの各アームと比較すると、 Coin::Quater(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のような見た目になります。


# #![allow(unused_variables)]
#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)になります。そして、これをマッチの各アームと比較します。

None => None,

Some(5)という値は、Noneというパターンにはマッチしませんので、次のアームに処理が移ります。

Some(i) => Some(i + 1),

Some(5)Some(i)にマッチしますか?なんと、します!列挙子が同じです。iSomeに含まれる値に束縛されるので、 iは値5になります。それから、このマッチのアームのコードが実行されるので、iの値に1を足し、 合計の6を中身にした新しいSome値を生成します。

さて、xNoneになるリスト6-5の2回目のplus_oneの呼び出しを考えましょう。matchに入り、 最初のアームと比較します。

None => None,

マッチします!足し算する値がないので、プログラムは停止し、=>の右辺にあるNone値が返ります。 最初のアームがマッチしたため、他のアームは比較されません。

matchとenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう: enumに対しmatchし、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、 一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。

マッチは包括的

もう一つ議論する必要のあるmatchの観点があります。一点バグがありコンパイルできないこんなバージョンのplus_one関数を考えてください:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

Noneの場合を扱っていないため、このコードはバグを生みます。幸い、コンパイラが捕捉できるバグです。 このコードのコンパイルを試みると、こんなエラーが出ます:

error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`がカバーされてません)
 -->
  |
6 |         match x {
  |               ^ pattern `None` not covered

全可能性を網羅していないことをコンパイラは検知しています。もっと言えば、どのパターンを忘れているかさえ知っているのです。 Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。 特にOption<T>の場合には、コンパイラが明示的にNoneの場合を扱うのを忘れないようにする時、 nullになるかもしれない値があることを想定しないように、故に、前に議論した10億ドルの失敗を犯さないよう、 保護してくれるわけです。

_というプレースホルダー

Rustには、全ての可能性を列挙したくない時に使用できるパターンもあります。例えば、u8は、有効な値として、 0から255までを取ります。1、3、5、7の値にだけ興味があったら、0、2、4、6、8、9と255までの数値を列挙する必要に迫られたくはないです。 幸運なことに、する必要はありません: 代わりに特別なパターンの_を使用できます:


# #![allow(unused_variables)]
#fn main() {
let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}
#}

_というパターンは、どんな値にもマッチします。他のアームの後に記述することで、_は、 それまでに指定されていない全ての可能性にマッチします。()は、ただのユニット値なので、_の場合には、 何も起こりません。結果として、_プレースホルダーの前に列挙していない可能性全てに対しては、 何もしたくないと言えるわけです。

ですが、一つのケースにしか興味がないような場面では、match式はちょっと長ったらしすぎます。 このような場面用に、Rustには、if letが用意されています。