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
による簡潔な条件分岐、そして多様なパターン構文を使いこなすことで、より堅牢で表現力豊かなプログラムを書くことができます。
最初は少し複雑に感じるかもしれませんが、使い慣れるとその便利さに気づくはずです。ぜひ色々なパターンを試してみてください!