【Rust】条件分岐とパターンマッチング

今回はRustにおける条件分岐処理についてまとめました。

Rustではifなどの基本的な構文からif let, matchといった特有の処理など様々な方法を使用して条件分岐させることができます。

そんな条件分岐処理の数々をサンプルコード多数で紹介しますのでぜひご覧になってください!

この記事でわかること
  • 基本的な条件分岐処理
  • Rust特有の条件分岐処理
  • 条件分岐で使用できるパターン
目次

if

まずはじめに基本的な条件分岐処理であるifを見ていきます。

基本形

基本的な記述は以下のようになります。

fn sample() {
    let x = 20;

    if x < 10 {
        println!("x is less then 10");
    } else if x > 20 {
        println!("x is greater then 20");
    } else {
        println!("x is between 10 and 20");
    }
}

他言語との違い

Rustのif戻り値を持ちます。

そのため以下のようなコードで条件に合わせた値を変数へ束縛することができます。

fn sample() {
    let x = 20;
    let y = if x > 10 { 30 } else { 50 };

    println!("{y}");
    // -> 30
}

Rustには三項演算子が存在しません。
同様の処理を行う場合は上記のlet ifや後述のmatchを使用します。

参考リンク

match

Rustには他言語に存在するswtich case構文が存在しません。

厳密には異なりますが、似た処理としてmatchという構文が使用できます。

基本形

基本的な記述は以下のようになります。

fn sample() {
    let x = 20;
    match x {
        10 => println!("ten"),
        20 => println!("twenty"),
        _ => println!("else"), // _(アンダースコア)は上記で記述したパターン以外のすべてにマッチする
    };
    // -> twenty
}

match内の各条件分岐(パターン => 式)部分はマッチアームもしくはアームと言います。

matchの注意点として、マッチさせる変数(上記のサンプルコードではx)が取りうるすべての値のパターンをアームとして記述する必要があります。すべてのパターンが網羅されていない場合はコンパイルエラーとなります。

fn sample() {
    let x: i32 = 20;

    // 下記のmatchではマッチする値が10と20のパターンしかない。
    // i32型が取りうる他の値について網羅されていないためコンパイルエラーとなる。
    match x {
        10 => println!("ten"),
        20 => println!("twenty"),
    };
}

一部のパターンのみマッチさせたいという場合は後述するif letが適しています。

戻り値

matchも前述のifと同様に戻り値を持ちます。

そのためletと組み合わせることで条件に応じた変数束縛することができます。

fn sample() {
    let x = 20;
    let y = match x {
        10 => 30,
        20 => 50,
        _ => -1,
    };

    println!("{y}");
    // -> 50
}

変数マッチと値の束縛

matchの良いところとして、パターンに変数を指定することで特定のリテラル値ではなく包括的な値をマッチできるということが挙げられます。

具体的にはOptionResultといった値を内包する型にマッチさせながら、内包する値を取り出してアーム内で使用することができます。

fn sample() {
    let some = Some(5);

    match some {
        // Someの中身がどんな値であろうとここにマッチする。さらに中身の値を変数xに束縛する。
        Some(x) => {
            // xはこのアームの式で使用することができる
            let y = x * x;
            println!("{y}"); // -> 25
        }
        None => {
            println!("None");
        }
    }
}

複雑な条件分岐

前述の変数マッチはすべての値にマッチしてしまいます。

マッチする値を絞り込みたい場合は次のマッチガードバインディングを使用できます。

マッチガード

各アームにはマッチガードと呼ばれる追加条件を指定することができます。

マッチガードは各アームの条件式部分にifを追加することで実現できます。

fn sample() {
    let some = Some(11);

    match some {
        Some(x) if x > 10 => {
            // xが10より大きい場合はこのブロックが実行される
            println!("x > 10");
        }
        Some(x) if x < 0 => {
            // xが上記のマッチガードに当てはまらない場合かつ、0より小さい場合はこのブロックが実行される
            println!("x < 0");
        }
        Some(x) => {
            // xが上記のマッチガードに当てはまらない場合はこのブロックが実行される
            println!("{x}");
        }
        None => {
            println!("None");
        }
    }
}

バインディング

バインディングと呼ばれる手法でもマッチする値を絞り込めます。

バインディングは以下のように変数の後ろに@を記述する形式となります。

fn sample() {
    let some = Some(10);

    match some {
        Some(x @ 10..=20) => {
            // xが10~20の場合はこのブロックが実行される
            println!("x = {x}");
        }
        Some(x) => {
            // xが上記のマッチガードに当てはまらない場合はこのブロックが実行される
            println!("{x}");
        }
        None => {
            println!("None");
        }
    }
}

マッチガードとバインディングの違い

マッチガードとバインディングはどう使い分ければ良いのでしょうか?

そんな疑問に少しでも役に立てればと思い、大きな違いを3つほどまとめました。

網羅性
マッチガード

マッチガードによる条件が網羅的であっても、match構文としての網羅性としては考慮されない。そのためマッチガードで対象の値が取りうるすべての値のパターンについて網羅されていても、match構文としてのパターンが不十分であればコンパイルエラーとなる。

バインディング

@で適用される条件がmatch構文の網羅性として考慮される。そのためバインディングが網羅的であればmatch構文として成り立ち、逆に網羅的でなければmatch構文として不十分となるのでコンパイルエラーとなる。

fn sample() {
    let some: Option<i32> = Some(10);

    match some {
        Some(v) if v > 0 => println!("{v}"),
        Some(v) if v <= 0 => println!("{v}"),
        // 上記まででSome(v)が取りうるすべての値は網羅されている。
        // しかし次のアーム(すべての値にマッチするもの)がないとコンパイルエラーとなる。
        Some(_) => println!("never reach"),
        None => println!("None"),
    }

    match some {
        // 1以上の数値を対象とするアーム
        Some(v @ 1..) => println!("{v}"),
        // 0以下の数値を対象とするアーム
        Some(v @ i32::MIN..=0) => println!("{v}"),
        None => println!("None"),
        // 上記まででOption<i32>が取りうるすべての値を網羅しているため問題なし。
    }
}
変数の束縛
マッチガード

マッチガード内で新たに変数束縛することはできない。

バインディング

バインディングで新たに変数束縛することができる。

fn sample() {
    let some: Option<i32> = Some(10);

    match some {
        // マッチガード(if)以降で新たな変数(t)を束縛することはできない。
        v if Some(t).unwrap() > 10 => println!("{:?}", v),
        Some(v) => println!("{v}"),
        None => println!("None"),
    }

    match some {
        // バインディング(@)以降で新たな変数(t)を束縛することができる。
        v @ Some(t @ ..=10) => println!("{:?},{:?}", v, t),
        None => println!("None"),
        _ => println!("other"),
    }
}
複雑な条件
マッチガード

マッチと関係ない条件が記述できる

バインディング

マッチと関係ない条件は記述できない

fn sample() {
    let some: Option<i32> = Some(10);
    let is_enable = false;

    match some {
        // マッチガード(if)にて束縛変数(v)とは無関係の条件を記述できる。
        Some(v) if v > 10 && is_enable => println!("{v}"),
        None => println!("None"),
        _ => println!("other"),
    }

    match some {
        // バインディング(@)では束縛変数(v)と無関係の条件は記述できない。
        v @ Some(t) && is_enable => println!("{t}"),
        None => println!("None"),
        _ => println!("other"),
    }
}

if let

matchでは対象の値が取りうるパターンすべてを網羅する必要がありました。

そのため一部のみマッチしたい場合などは冗長なコードとなることもあります。

そんな場合はif letを使用することで簡潔に記述することができます。

fn sample() {
    let some = Some(35);

    match some {
        Some(3) => println!("3"),              // 直接マッチ(単一パターン)
        Some(10 | 20) => println!("10 or 20"), // 直接マッチ(複数パターン)
        Some(20..=30) => println!("21 to 30"), // 直接マッチ(範囲)
        Some(x @ 35) => println!("{x}"),       // バインディング(単一パターン)
        Some(x @ (36 | 37)) => println!("{x}"),  // バインディング(複数パターン)
        Some(x @ 38..=80) => println!("{x}"),  // バインディング(範囲)
        Some(x) => println!("{:?}", x * x),    // 変数マッチ(すべての値)
        _ => println!("other"),                // 上記以外のすべての値
    }
}

let else

次にRust1.65にて新たに導入された構文であるlet elseを見てみましょう。

基本的な使い方は以下のようになります。

fn sample() {
    let option: Option<i32> = Some(10);

    // Some(v)のパターンにマッチするのでsome_valueに値が束縛され、elseは実行されない
    let Some(some_value) = option else {
        println!("else");
        return
    };
    println!("{:?}", some_value); // -> 10

    // Noneのパターンにマッチしないのでelseが実行される
    let None = option else {
        println!("else");
        return;
    };

    // 上記のelseでreturnしているのでここには到達しない
    println!("never reach");
}

if letとの違い

上記のサンプルコードを見るとわかりますが、条件にマッチして束縛した変数(サンプルコードのsome_value)をそのまま現在のスコープで使用しています。

これに対してif letでは束縛した変数は分岐処理内のブロックでしか使用できません。

fn sample() {
    let some: Option<i32> = Some(10);
    if let Some(i) = some {
        // 束縛した変数iはこのブロック内でしか使えない
        println!("{:?}", i);
    }

    // 以下のようにしても変数iは分岐処理外のスコープには存在しないためコンパイルエラーとなる。
    println!("{:?}", i);
}

let ifとの違い

let elseと似た構文にlet ifというものがありました。(サンプルコード)

これらの違いとしてはパターンマッチを使用した変数束縛ができるか否かというところです。

fn sample() {
    let option: Option<i32> = Some(10);

    // Someの中身を取り出すことができる。Noneの場合はelseが実行される。
    let Some(some_value) = option else {
        println!("else");
        return
    };

    // 以下のようには記述できない。
    let Some(v) = if true { Some(2) } else { Some(5) };
}

値の絞り込み

バインディングを使用することでlet elseでもマッチさせたい値を絞り込むことができます。

fn sample() {
    let option: Option<i32> = Some(0);

    // Someの中身が10~20の場合は変数束縛が実行され、それ以外はelseが実行される
    let Some(some_value @ 10..=20) = option else {
        println!("else");
        return
    };
}

elseの戻り値

elseブロックからの戻り値はlet elseが存在する関数の戻り値と同じ型!型(never型)である必要があります。

例:関数の戻り値がi32の場合

fn main() {
    let x = sample();
}

fn sample() -> i32 {
    let option: Option<i32> = None;

    let Some(_x) = option else {
        return -1
    };

    let Some(_y) = option else {
        panic!("else");
    };

    0
}

例:関数の戻り値がない(()型となる)場合

fn main() {
    sample();
}

fn sample() {
    let option: Option<i32> = None;

    let Some(_x) = option else {
        return
    };

    let Some(_y) = option else {
        panic!("else");
    };
}

上記で使用しているpanic!が返す型は!型(never型)となります。

参考リンク

while let

while letという構文を用いることで、ループ構造であるwhileにおいても変数マッチを行うことができます。


fn sample() {
    let mut vec = vec![1, 2, 3];

    while let Some(x) = vec.pop() {
        println!("{:?}", x);
        // -> 3
        // -> 2
        // -> 1
    }
}
参考リンク

使用可能なパターンの例

最後に、今回紹介したmatch,if let,let else,while letで使用できるパターンをまとめてみました。

fn sample() {
    let some = Some(35);

    match some {
        Some(3) => println!("3"),              // 直接マッチ(単一パターン)
        Some(10 | 20) => println!("10 or 20"), // 直接マッチ(複数パターン)
        Some(20..=30) => println!("21 to 30"), // 直接マッチ(範囲)
        Some(x @ 35) => println!("{x}"),       // バインディング(単一パターン)
        Some(x @ (36 | 37)) => println!("{x}"),  // バインディング(複数パターン)
        Some(x @ 38..=80) => println!("{x}"),  // バインディング(範囲)
        Some(x) => println!("{:?}", x * x),    // 変数マッチ(すべての値)
        _ => println!("other"),                // 上記以外のすべての値
    }
}

最後に

様々な条件分岐処理があり、それぞれの違いやニュアンスを理解するのが大変でした。。。

しかしRustの分岐処理はコードを簡潔にできるとともにスコープも限定されてとても良いです。

バージョンアップを重ねてさらに便利な条件分岐処理も増えています。

より簡潔かつ堅牢なコードを書くためにもキャッチアップを続けていきたいところです。

よかったらシェアしてね!
  • URLをコピーしました!
目次