[Rustのはじめ方] Part18: パターンマッチと分岐処理

Rust

match式やif letを使いこなして、より表現力豊かなコードを書こう!

パターンマッチとは?🤔

パターンマッチは、Rustの強力な機能の一つで、値の構造と特定のパターンを比較し、一致した場合に対応するコードを実行する仕組みです。これにより、複雑な条件分岐を簡潔かつ安全に記述できます。特にmatch式で頻繁に利用されます。

他の言語のswitch文に似ていますが、Rustのパターンマッチはより高機能で、様々な種類のパターンに対応しています。

match式でのパターンマッチ

match式は、ある値を取り、その値が取りうる可能性のあるパターンを列挙し、一致したパターンのコードブロックを実行します。重要なのは、match式は網羅的でなければならないことです。つまり、全ての可能性をカバーする必要があります。

基本的な例

列挙型(enum)とmatchの組み合わせは非常によく使われます。

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,
    }
}

let my_coin = Coin::Dime;
let value = value_in_cents(my_coin);
println!("The value is: {}", value); // The value is: 10

上記の例では、coin変数の値(Coin::Penny, Coin::Nickelなど)と各アーム(=>の左側)のパターンを比較し、一致したアームの右側の式を実行します。Coin::Pennyのアームでは、複数の文を実行するために波括弧{}が使われています。

値を束縛するパターン

パターン内で変数名を指定することで、マッチした値の一部または全体を新しい変数に束縛できます。

#[derive(Debug)] // すぐに州を出力できるようにするため
enum UsState {
    Alabama,
    Alaska,
    // ... など
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // Quarterは州の情報を持つ
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => { // state変数にUsStateの値を束縛
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

let coin1 = Coin::Quarter(UsState::Alaska);
value_in_cents(coin1); // State quarter from Alaska! と出力

Option<T>とのマッチ

Option<T>型(値が存在するかもしれないし、しないかもしれないことを表す型)のマッチングは非常に一般的です。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,       // 値がない場合(None)
        Some(i) => Some(i + 1), // 値がある場合(Some)、中の値iに1を足す
    }
}

let five = Some(5);
let six = plus_one(five);    // Some(6)
let none = plus_one(None); // None

Option<T>のような型を使うことで、nullポインタエラーのような問題をコンパイル時に防ぐことができます🛡️。

網羅性チェックと_プレースホルダー

match式は網羅的である必要があるため、全ての可能性をリストアップしなければなりません。しかし、特定のケース以外は同じ処理をしたい場合や、特定の値を無視したい場合があります。その際に_(アンダースコア)プレースホルダーが役立ちます。_はどんな値にもマッチしますが、その値を束縛することはありません。

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other), // other変数に束縛して使う
    // _ => reroll(),           // その他の値は無視してreroll()を呼ぶ
    // _ => (),                 // その他の値は何もしない (Unit値を返す)
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
// fn reroll() {}

otherのような変数名を使うと、マッチした値を使用できます。もし値を使う必要がなければ、_を使うことで、コンパイラに値を使用しない意図を明確に伝えられます。

if letによる簡潔な分岐処理

match式は非常に強力ですが、特定の1つのパターンにだけマッチするかどうかを調べ、それ以外の場合は何もしない、というような状況では少し冗長になることがあります。このような場合にif let構文が便利です。

let config_max = Some(3u8);

// match式の場合
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (), // 何もしないアームが必要
}

// if let式の場合 ✨
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}
// 値がNoneの場合は何もしない

if letは、=の左側のパターンが右側の式(この例ではconfig_max)とマッチした場合に、続く{}ブロック内のコードを実行します。マッチしなかった場合は何もしません。

if letにはelseブロックを追加することも可能です。

let mut count = 0;
let coin = Coin::Quarter(UsState::Alaska);

// if let else を使った例
if let Coin::Quarter(state) = coin {
   println!("State quarter from {:?}!", state);
} else {
   count += 1; // Quarterでなければカウントアップ
}

これは以下のmatch式と等価です。

let mut count = 0;
let coin = Coin::Quarter(UsState::Alaska);

// match式で書いた場合
match coin {
    Coin::Quarter(state) => {
        println!("State quarter from {:?}!", state);
    },
    _ => { // その他のCoinバリアントの場合
        count += 1;
    }
}

if letを使うことで、1つのパターンだけを扱う場合にコードをより簡潔にできますね!🎉

while letによるループ処理

if letと同様に、while letはループ条件としてパターンマッチを利用します。パターンがマッチし続ける限り、ループ内のコードを実行します。

これは、例えばVec(ベクタ、可変長の配列)からpop()メソッドで要素を取り出し続けるような場合に便利です。pop()Option<T>を返し、要素があればSome(T)、なければNoneを返します。

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

// while let でスタックから要素を取り出す
while let Some(top) = stack.pop() {
    println!("{}", top); // 3, 2, 1 の順で出力される
}
// ループはstack.pop()がNoneを返した時点で終了する

while letを使うことで、loop, match, breakを組み合わせるよりも簡潔に書けます。

その他のパターン構文

Rustのパターンマッチは非常に多様な構文をサポートしています。

パターン 説明
リテラル 具体的な値とのマッチ。 1, 'a', "hello"
変数束縛 任意の値にマッチし、その値を新しい変数に束縛する。 x, value
_ 任意の単一の値にマッチするが、値は使用しない(無視する)。 _
構造体パターン 構造体のフィールドに対するマッチ。{ field1, field2, .. }のように書く。..は残りのフィールドを無視する。 Point { x: 0, y }
列挙型パターン 列挙型のバリアントに対するマッチ。 Option::Some(x), Result::Ok(v)
タプルパターン タプルの要素に対するマッチ。 (0, y, z)
配列/スライスパターン 配列やスライスの要素に対するマッチ。固定長、可変長([first, .., last])など。 [1, 2, 3], [first, rest @ ..]
| (OR) 複数のパターンのいずれかにマッチ。 1 | 2, Some(x) | None
..= (範囲) 特定の範囲内の値にマッチ(数値や文字リテラルのみ)。 1..=5, 'a'..='z'
@ (束縛) パターンの一部または全体にマッチさせつつ、その値を別の変数名に束縛する。 e @ 1..=5, ref name @ Some(_)
ref, ref mut 値の所有権を奪う代わりに、参照を束縛する。 ref name, ref mut age
ガード (if 条件) パターンにマッチした上で、さらに追加の条件 (if) を満たす場合にのみマッチする。 Some(x) if x > 10

これらのパターンを組み合わせることで、非常に複雑な条件も表現力豊かに記述できます。

ガード (if 条件) の例

let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x), // xが5未満の場合のみマッチ
    Some(x) => println!("{}", x),
    None => (),
}

let x = Some(5);
let y = 10;

match x {
    Some(n) if n == y => println!("Matched, n = {}", n), // xの中の値nが外部変数yと等しいかチェック
    _ => println!("Default case, x = {:?}", x),
}

@ 束縛の例

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    // idフィールドが3から7の範囲にある場合にマッチし、その値をid_variableに束縛
    Message::Hello { id: id_variable @ 3..=7 } => {
        println!("Found an id in range: {}", id_variable)
    },
    // idフィールドが10, 11, 12のいずれかの場合にマッチ
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    },
    // その他のMessage::Helloの場合
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}

まとめ 🚀

Rustのパターンマッチは、コードの可読性と安全性を高めるための非常に強力なツールです。match式による網羅的なチェックや、if let / while letによる簡潔な条件分岐、そして多様なパターン構文を使いこなすことで、より堅牢で表現力豊かなプログラムを書くことができます。

最初は少し複雑に感じるかもしれませんが、使い慣れるとその便利さに気づくはずです。ぜひ色々なパターンを試してみてください!😊

コメント

タイトルとURLをコピーしました