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