Rust入門

【Rust入門】パターンマッチングの基本について分かりやすく解説

【Rust入門】パターンマッチングの基本について分かりやすく解説
naoki-hn

Rust のパターンマッチング(matchの基本について初心者にも分かりやすく解説します。

Rust のパターンマッチング

Rust には、一連のパターンに対して比較をしてマッチしたパターンに応じて処理を実行するパターンマッチング(matchという非常に強力な機能があります。この機能は、特に列挙型(enum)と連携することで真価を発揮します。

C/C++ などの他の言語にある switch 文のような条件分岐に似ていますが、Rust のパターンマッチでは型の構造の中まで考慮したマッチングが可能でより強力なものです。

この記事では、Rust のパターンマッチングの基本的な使い方と代表的な使用例を紹介します。

列挙型の基本については以下を参考にしてください。

【Rust入門】列挙型の基本を分かりやすく解説

Haskell や Scala といった関数型プログラミング言語は非常に強力なパターンマッチングの機能を持っており、Rust は関数型プログラミング言語の特徴を強く取り入れている言語です。また、Python も 3.10 以降でパターンマッチングの構文が導入されています。

match の基本的な使い方

Rust の match の基本的な構文は以下のようになります。

match の基本構文
match 条件 {
    パターン1 => {
        // パターン1 の時の処理
    },
    パターン2 => {
        // パターン2 の時の処理
    },
    _ => {
        // 上記のパターンに一致しない場合の処理
    },
}

「条件」の部分には、リテラル値、型のインスタンス、変数、式、関数の戻り値などの様々なものを使用できます。指定した条件が「パターン」に一致した場合の処理を「=>」以降に記載します。なお、どのパターンにも一致しない場合には「_」を使うことで、その他の全てのパターンを捉えることができます。

以下は、他のプログラミングでもよく出てくる簡単な分岐処理を match で実現した例です。

fn main() {
    let value = 2;

    // パターンマッチ
    match value {
        1 => println!("失敗"),
        // ↓ この部分がマッチする
        2 => println!("成功"),
        _ => println!("値は不正値です。"),
    }
}
【実行結果】
成功

上記では、value = 2 なので「成功」と表示されました。このような条件分岐は if 文でももちろん実現できますが、match の方が可読性高く書くことができます。

上記は非常にシンプルな例ですが、Rust のパターンマッチは列挙型とあわせて使用したときに強力な表現力を実現します。以降で順に紹介していきます。

列挙型に対するマッチング

以下は、曜日を表す列挙型 WeekDay に対して match を使用して分岐をする場合の例です。

// 曜日を表す列挙型
enum WeekDay {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
}

// 曜日を表示する
fn show_day_of_week(weekday: &WeekDay) {
    // パターンマッチングで条件分岐
    match weekday {
        WeekDay::Sunday => println!("日曜日"),
        WeekDay::Monday => println!("月曜日"),
        WeekDay::Tuesday => println!("火曜日"),
        WeekDay::Wednesday => println!("水曜日"),
        WeekDay::Thursday => println!("木曜日"),
        WeekDay::Friday => println!("金曜日"),
        WeekDay::Saturday => println!("土曜日"),
    }
}

fn main() {
    let sunday = WeekDay::Sunday;
    show_day_of_week(&sunday);

    let monday = WeekDay::Monday;
    show_day_of_week(&monday);

    let tuesday = WeekDay::Tuesday;
    show_day_of_week(&tuesday);

    let wednesday = WeekDay::Wednesday;
    show_day_of_week(&wednesday);

    let thursday = WeekDay::Thursday;
    show_day_of_week(&thursday);

    let friday = WeekDay::Friday;
    show_day_of_week(&friday);

    let saturday = WeekDay::Saturday;
    show_day_of_week(&saturday);
}
【実行結果】
日曜日
月曜日
火曜日
水曜日
木曜日
金曜日
土曜日

上記例のように、WeekDay::Sunday といった列挙型のバリアントに対して match による分岐処理を記載することができます。Rust のパターンマッチングで強力であるのは、コンパイル時に全てのパターンを網羅しているかどうかをチェックしてくれる点です。

例えば、以下のように一部のバリアントに対する処理をコメントアウトしてコンパイルしてみてください。エラーが表示されてコンパイルできません。

// 曜日のを表示する
fn show_day_of_week(weekday: &WeekDay) {
    // パターンマッチングで条件分岐
    match weekday {
        WeekDay::Sunday => println!("日曜日"),
        WeekDay::Monday => println!("月曜日"),
        WeekDay::Tuesday => println!("火曜日"),
        // WeekDay::Wednesday => println!("水曜日"),
        // WeekDay::Thursday => println!("木曜日"),
        // WeekDay::Friday => println!("金曜日"),
        // WeekDay::Saturday => println!("土曜日"),
    }
}
【コンパイル結果】
error[E0004]: non-exhaustive patterns: `&WeekDay::Wednesday`, `&WeekDay::Thursday`, `&WeekDay::Friday` and 1 more not covered
  --> examples\pattern_matching_basic_1.rs:15:11
   |
15 |     match weekday {
   |           ^^^^^^^ patterns `&WeekDay::Wednesday`, `&WeekDay::Thursday`, `&WeekDay::Friday` and 1 more not covered
   |
note: `WeekDay` defined here
  --> examples\pattern_matching_basic_1.rs:2:6
   |
2  | enum WeekDay {
   |      ^^^^^^^
...
6  |     Wednesday,
   |     --------- not covered
7  |     Thursday,
   |     -------- not covered
8  |     Friday,
   |     ------ not covered
9  |     Saturday,
   |     -------- not covered
   = note: the matched value is of type `&WeekDay`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown, or multiple match arms
   |
18 ~         WeekDay::Tuesday => println!("火曜日"),
19 ~         _ => todo!(),
   |

上記のようにコンパイラが、型で取り得るパターンを考慮してチェックしてエラーを出してくれるため、プログラマの考慮漏れに気づくことができます。これにより、バグの少ない安全なプログラミングをできるようになります。

型の構造を含めたパターンマッチング

上記は単純な列挙型に対するマッチングでしたが、Rust ではより強力に型の構造を含めたパターンマッチングができます。Rust の列挙型は、値をもったバリアントを定義できることが特徴的です。パターンマッチングでは、バリアントの型の構造を含めたマッチングが可能です。

以下は、メッセージを表す列挙型の Message 型を定義し、そのバリアントによって match で処理を分岐している例です。

// メッセージを表す列挙型
enum Message {
    Quit,
    Echo(String),
    Move(i32, i32),
    ChangeColor { r: u8, g: u8, b: u8, a: f32 },
}

// メッセージに対する処理を実施
fn action_for_message(message: &Message) {
    // 構造を含めたパターンマッチ
    match message {
        Message::Quit => println!("終了します。"),
        Message::Echo(s) => println!("出力: {}", s),
        Message::Move(x, y) => println!("移動: ({}, {})", x, y),
        Message::ChangeColor{r, g, b, a} =>println!("色 (r, g, b, a) = ({}, {}, {}, {})", r, g, b, a),
    }
}

fn main() {
    let message1 = Message::Quit;
    action_for_message(&message1);

    let message2 = Message::Echo(String::from("パターンマッチング"));
    action_for_message(&message2);

    let message3 = Message::Move(5, 10);
    action_for_message(&message3);

    let message4 = Message::ChangeColor{ r: 255, g: 255, b: 0, a: 0.5 };
    action_for_message(&message4);
}
【実行結果】
終了します。
出力: パターンマッチング
移動: (5, 10)
色 (r, g, b, a) = (255, 255, 0, 0.5)

上記で特徴的なのは「Message::Echo(s)」「Message::Move(x, y)」「Message::ChangeColor{r, g, b, a}」のように、条件に渡された型のインスタンス値を変数に束縛している点です。これにより、パターンに一致した処理の中で、その変数を使用した処理をすることができます。

このように、型の内部構造まで含めてパターンマッチングするような強力なマッチングは、C/C++ などの switch では実現できません。

使わない項目は _ でまとめて処理

冒頭の例でも簡単に紹介しましたが「_」は、指定されていない全てのパターンにマッチする汎用的なパターンです。以下の例は、Message::Quit 以外の場合は何も実行しないようにしている例です。

// メッセージに対する処理を実施
fn quit_only(message: &Message) {
    // 構造を含めたパターンマッチ
    match message {
        Message::Quit => println!("終了します。"),
        _ => (),  // Quit以外は何もしない
    }
}

fn main() {
    let message1 = Message::Quit;
    quit_only(&message1);

    let message2 = Message::Echo(String::from("パターンマッチング"));
    quit_only(&message2);

    let message3 = Message::Move(5, 10);
    quit_only(&message3);

    let message4 = Message::ChangeColor{ r: 255, g: 255, b: 0, a: 0.5 };
    quit_only(&message4);
}
【実行結果】
終了します。

_」によるパターンは、明確にその他パターンを無視してよいことが分かっている場合にのみ使うようにしましょう。Rustは、コンパイラで網羅的にパターンに対する処理が書かれているかチェックしてくれることが非常に強力な点です。安易に「_」としてしまうと、本当は処理が必要なパターンを見落としてしまう可能性があります。

match を式として変数に値を束縛する

match は式であるため、その結果を返しつつ変数に束縛することができます。以下の例では、パターンにマッチしたら文字列を出力しつつ、各バリアントに対するコード値を返しているような例です。

// メッセージに対する処理を実施
fn action_for_message(message: &Message) -> u8 {
    // 構造を含めたパターンマッチ
    // match 式として値を返却する
    match message {
        Message::Quit => {
            println!("終了します。");
            0
        },
        Message::Echo(s) => {
            println!("出力: {}", s);
            1
        },
        Message::Move(x, y) => {
            println!("移動: ({}, {})", x, y);
            2
        },
        Message::ChangeColor{r, g, b, a} => {
            println!("色 (r, g, b, a) = ({}, {}, {}, {})", r, g, b, a);
            3
        },
    }
}

fn main() {
    // match の結果を code 変数に束縛する
    let message1 = Message::Quit;
    let code = action_for_message(&message1);
    println!("code: {}", code);

    let message2 = Message::Echo(String::from("パターンマッチング"));
    let code = action_for_message(&message2);
    println!("code: {}", code);

    let message3 = Message::Move(5, 10);
    let code = action_for_message(&message3);
    println!("code: {}", code);

    let message4 = Message::ChangeColor{ r: 255, g: 255, b: 0, a: 0.5 };
    let code = action_for_message(&message4);
    println!("code: {}", code);
}
【実行結果】
終了します。
code: 0
出力: パターンマッチング
code: 1
移動: (5, 10)
code: 2
色 (r, g, b, a) = (255, 255, 0, 0.5)
code: 3

action_for_message 関数の返却値は match の結果の値をそのまま返却しており、呼び出し元の main 関数では、返却値を変数 code に束縛して使用しています。

if let での簡潔なパターンマッチング

パターンマッチングは非常に強力な機能ですが、ある特定のパターンにのみ一致した場合にのみ処理をしたい場合に毎回すべてのパターンを記載するのは少し面倒です。

そのような場合に便利で簡潔なパターンマッチとして「if let」という構文が用意されています。以下は、Message::Echo(s) のパターンに一致するときにのみ処理を実行し、その他のパターンは無視するような例です。

// メッセージに対する処理を実施
fn echo_message(message: &Message) {
    // if let を使用して Echo(s) パターンのみにマッチング
    if let Message::Echo(s) = message {
        println!("出力: {}", s);
    }
}

fn main() {
    let message1 = Message::Quit;
    echo_message(&message1);

    let message2 = Message::Echo(String::from("パターンマッチング"));
    echo_message(&message2);

    let message3 = Message::Move(5, 10);
    echo_message(&message3);

    let message4 = Message::ChangeColor{ r: 255, g: 255, b: 0, a: 0.5 };
    echo_message(&message4);
}
【実行結果】
出力: パターンマッチング

この例は、match にて Message::Echo(s) 以外のパターンを「_」で無視するのと同じです。

    match message {
        Message::Echo(s) => println!("出力: {}", s),
        _ => (),
    }

マッチガード(if)での条件を追加

match では、あるパターンに一致した中でさらに if により条件を指定することができます。このような if 条件のことをマッチガード(match guard)と言います。

以下の例は、Message::Move(x, y)x == y の時のみ表示を変更している例です。

fn move_action(message: &Message) {
    match message {
        Message::Move(x,y) if x == y => println!("対角方向への移動です: ({}, {})", x, y),
        Message::Move(x,y) => println!("移動先: ({}, {})", x, y),
        _ => (),
    }
}

fn main() {
    let message = Message::Move(5, 10);
    move_action(&message);
    
    let message = Message::Move(5, 5);
    move_action(&message);
}
【実行結果】
移動先: (5, 10)
対角方向への移動です: (5, 5)

上記で色々な例で紹介してきたように Rust の match は非常に広いパターンに対応できる強力なパターンマッチング機能となっています。

パターンマッチングが強力である理由

上記のいくつかの例でパターンマッチングの基本的な使い方について紹介してきました。ここでは、Rust のパターンマッチングが強力である理由について改めて整理しておきましょう。

  1. 型の構造を分解してマッチングできる
    ただの値の一致だけではなく、構造体・列挙型・タプル・配列・スライスなどの中身を直接取り出して条件分岐できます。
  2. 全てのパターンを網羅的にチェックできる
    コンパイラが分岐の漏れを検出してくれるため、意図しないバグを未然に防ぐことができます。
  3. matchif let は式として使える
    式として値を返すので、結果を変数に直接束縛して使用することができます。
  4. マッチガード(if)により柔軟に条件を加えられる
    パターンに追加の条件を加えることができるので、分岐の表現力が非常に高いです。

他にもパターンマッチングの機能はありますが、入門としては上記のようなことを理解しておいてもらえれば十分かと思います。

代表的なパターンマッチングの使用例

Rust の列挙型として重要な型として Option<T> 型と Result<T, E> 型があります。これらの型とパターンマッチングの使い方を理解することは Rust プログラミングにおいて必須なため紹介します。(※ TE は「ジェネリクス」と呼ばれ、後から使う側が具体的な型を指定できる仕組みです。)

Option<T> 型のパターンマッチング

Option<T> 型は、値がある場合は「Some(T)」、値がない場合は「None」で表現する列挙型です。Option<T> 型に対して match でパターンマッチをする場合には以下のようにします。

fn show_maybe_number(maybe_number: &Option<i32>) {
    // Option 型のマッチングで処理を分岐
    match maybe_number {
        Some(x) => println!("数値: {}", x),
        None => println!("値がありません。"),
    }
}

fn main() {
    let maybe_number = Some(10);
    show_maybe_number(&maybe_number);

    let maybe_number = None;
    show_maybe_number(&maybe_number);
}
【実行結果】
数値: 10
値がありません。

上記の例では、Ti32 型としていますが、どんな型でも使えます。match では、値が存在する「Some(x)」 のパターンのときのみ数値を x に取り出して利用し、「None」の場合は、メッセージを表示しています。

世の中に存在するデータの中には値が設定されているかどうかわからないようなケースは非常に多く存在します。例えば、設備から取得されるようなデータは、センサの状況によりデータが欠損するような場合もあります。このような時には、Option<T> 型と match を活用することでどちらの状況になった場合でも適切な処理を実装することができます。

Result<T, E> 型のパターンマッチング

Result<T, E> 型は、成功した場合は「Ok(T)」、処理が失敗した場合は「Err(E)」で表現する列挙型です。関数等の戻り値などに非常によく利用されます。Rust では、他のプログラミング言語での例外の考え方がないため、エラー処理において、Result<T, E> 型の扱いを理解するのは必須です。

Result<T, E> 型に対して match でパターンマッチングをする場合には以下のようにします。

fn show_result(result: &Result<i32, String>) {
    // Result 型のマッチングで処理を分岐
    match result {
        Ok(x) => println!("結果: {}", x),
        Err(e) => println!("エラー: {}", e)
    }
}

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("0で割ることはできません"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let a = 10;
    let b = 2;
    let result = divide(a, b);
    show_result(&result);

    let a = 10;
    let b = 0;
    show_result(&divide(a, b));
}
【実行結果】
結果: 5
エラー: 0で割ることはできません。

上記の例では、Ti32 型で、EString 型としていますが、どんな型でも使えます。match では、処理結果が成功した「Ok(x)」のパターンのときのみ数値を x に取り出して利用し、「Err(e)」の場合は、e に設定される文字列(上記例では"0で割ることはできません")を表示します。

Rust のプログラムでは多くの場合、Result<T, E> 型と同様の型を定義している場合がほとんどです。独自エラー型を簡単に定義したい場合の thiserror や異なるエラー型をまとめて扱うための anyhow といった便利なクレートもあります。これらのクレートについては、また別途取り上げたいと思います。

Rust では、エラー処理のために Result<T, E> 型の理解と match による扱いは必須の項目なので、まずは基本的な内容を理解してもらえればと思います。

まとめ

Rust のパターンマッチング(match)の基本について解説しました。

Rust のパターンマッチングは、単なる条件分岐を超えた強力な機能であり、特に列挙型や構造体の中身を直接分解して条件分岐できる点や、全てのパターンをコンパイラが網羅的にチェックできる点が他の言語にはない強力なところです。

この記事では、matchif let の基本的な使用例や、Rust において重要な型である、Option<T>Result<T, E> 型への適用例まで紹介しました。Rust を安全かつ効率的に扱ううえで、パターンマッチングは欠かせない技術ですので、ぜひ自分でもコードを書きながら理解を深めてみてください。

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました