極Rust - ifとmatch
if
1. ifは「文」でなく「式」である
Rustのifにおける最大の特徴は、 値を返す ということです。これは関数型言語のパラダイムを受け継いでいます。
参考演算子の排除
Rustには参考演算子(condition ? a : b)が存在しません。なぜなら、ifそのものがその役割を完全かつより強力に果たせるからです。
// Rustにおいてこれは慣用句(Idiomatic)です
let number = if condition { 5 } else { 6 };
型の一貫性
ifが式であるため、すべての分岐は同じ型を返さなければなりません。
// コンパイルエラー!
// ifブロックは i32, elseブロックは &str
let x = if true { 5 } else { "six" };
しかし、ここで面白いのが、「Never型(!)」との相互作用です。break,continue,return,panic!などは!型(発散する型)を持ち、これは「あらゆる型にキャスト可能」とみなされます。
// これはコンパイルを通る!
// 5 (i32) と return (!) の比較において、全体は i32 と推論される
let x: i32 = if condition {
5
} else {
return; // または panic!("error");
};
2. unit型とセミコロンの罠
elseブロックがないif式は、暗黙的に()(unit型)を返すとみなされます。
if condition {
do_something();
}
// 内部的には以下と同じ
// if condition { do_something(); } else { () }
そして、初心者がよくハマるのが、ブロック末尾のセミコロンです。
let x = if true {
5; // セミコロンがあるため、このブロックの評価値は () になる
} else {
6 // このブロックの評価値は i32
};
// エラー: `if` and `else` have incompatible types
// expected `()`, found `integer`
3. if let: パターンマッチングの糖衣構文
ifは単なるブール値のチェックだけでなく、パターンマッチングの簡易版としても機能します。
if let Some(x) = some_option {
println!("値は {} です", x);
}
内部挙動(HIR/MIRレベル)
コンパイラの中間表現(HIR/MIR)レベルでは、if let は match 式へと脱糖(desugar)されます。 つまり、以下の2つはコンパイラにとってほぼ等価です。
if let
if let Some(x) = opt { block } else { else_block }
match
match opt {
Some(x) => { block },
_ => { else_block } // _ (ワイルドカード) が必須
}
注意点 : match は「網羅性(Exhaustiveness)」を強制しますが、if let は「指定したパターン以外は無視する」という挙動のため、将来的にEnumのバリアントが増えた際に見落とすリスクがあります。
4. let else: ガード節の強化
if の親戚である let else は制御フローにおいて極めて重要です。これは「パターンにマッチしなかった場合に分岐する(そして現在のスコープから脱出する)」ことを強制します。
// ネストが深くなる `if let`
if let Some(val) = x {
process(val);
}
// フラットに書ける `let else`
let Some(val) = x else {
return; // 必須: Diverging (発散) しなければならない
};
process(val); // 外でのみ使える
少し違いが分かりづらいですよね。ポイントをまとめましょう。
-
if let- 宣言した変数はネストしたスコープ内でのみ使える
- マッチしなかった場合は「何もしない」か
elseに進む - マッチしなくても処理は続行する
-
let else- 失敗したら必ず発散が必要(
return,break等) - 宣言した変数はネストしたスコープ外でのみ使える
- 失敗したら必ず発散が必要(
5. const if
constやstaticでの定義でもifは完全に動作します。
const CONDITION: bool = true;
const X: u8 = if CONDITION { 1 } else { 0 };
const Y: u8 = if CONDITION { 0 } else { 1 };
fn main() {
println!("{X} {Y}");
}
ポイント: if そのものは単純な分岐ですが、const コンテキスト内では、分岐条件も分岐内の処理もすべて定数として評価可能である必要があります。Rustはバージョンアップごとに const 内でできること(ループ、アロケーションなど)を増やしており、if はその基盤となっています。
match
1. match の本質:式(Expression)としての評価
他の多くの言語と異なり、Rustの match はifと同じで、「文(Statement)」ではなく「式(Expression)」です。
- 値の返却:
全てのアーム(分岐)は同じ型を返す必要があります(!型、つまりDiverging functionを除く)。 - 末尾呼び出し最適化:
コンパイラはmatchの結果をレジスタに直接配置するように最適化を行うため、変数の再代入コストが発生しにくい構造になっています。
// 文ではなく式なので、結果を変数に束縛できる
let classification = match input {
0 => "Zero",
_ => "Non-zero",
};
2. メモリと所有権:Binding Modes(束縛モード)
match の理解で最も重要なのが、「マッチした値をどう扱うか」というBinding Modeです。ここで初心者が躓きやすく、上級者が技巧を凝らすポイントです。
- A. Match Ergonomics(自動参照)
現代のRustでは、マッチ対象が参照の場合、アーム内の変数も自動的に参照になります。
let x = Some(String::from("Hello"));
// &x は &Option<String> 型
match &x {
// ここで `s` は自動的に `&String` になる(ref s と書かなくてもよい)
Some(s) => println!("Used: {}", s),
None => (),
}
// x は move されていないのでここで使える
println!("{:?}", x);
- B. 明示的な制御 (ref, ref mut, move)
しかし、深堀りするなら「自動」に頼らず、手動制御を知る必要があります。特に 部分的な移動(Partial Move) を行いたい場合に必須です。
struct BigStruct {
id: i32,
data: String, // ヒープ確保された重いデータ
}
let bs = BigStruct { id: 1, data: "Huge...".into() };
match bs {
// id は Copy なので値としてコピー
// data は move したくないので `ref` で参照を取る
BigStruct { id, ref data } => {
println!("ID: {}, Data (borrowed): {}", id, data);
}
}
// bs.data は借用が終われば使えるが、bs 全体は部分的に消費されている扱いに注意
3. パターンマッチングの深層メカニズム
- A. @ バインディング(サブパターン束縛)
これは非常に強力ですが、見落とされがちな機能です。「値の範囲をテストしつつ、その値を別の変数に束縛する」ことができます。
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
// 3..=7 の範囲内であることを確認しつつ、その値を id_variable に束縛
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable);
}
Message::Hello { id: 10..=12 } => {
println!("Found id in 10-12");
}
_ => (),
}
- B. マッチガード(Match Guards)の罠と仕様
パターンマッチだけでは表現できない条件をifで追記できます。 重要な仕様 : ガード条件式の中で変数をmoveしてしまうと、後続のアームでその変数が使えなくなるため、コンパイルエラーになることがあります。
let x = Some(String::from("A"));
match x {
// ガード内で `n` の所有権を奪うような操作はできない
Some(ref n) if n.len() > 0 => println!("Non-empty"),
_ => (),
}
- C. スライスパターン(Slice Patterns)
可変長配列やベクタの中身を構造的に分解します。
let arr = [1, 2, 3, 4, 5];
match arr {
// 先頭、末尾、その間(middle)を抽出
[first, middle @ .., last] => {
println!("First: {}, Last: {}, Middle: {:?}", first, last, middle);
}
_ => (),
}
4. コンパイラ内部と最適化(MIR & LLVM)
Rustコンパイラ(rustc)が match をどう処理するかを知ると、パフォーマンス予測が立てやすくなります。
-
A. 網羅性チェック(Exhaustiveness Checking)
コンパイル時、Rustは「マトリックスアルゴリズム」を使用して、全ての可能なケースがカバーされているかを数学的に証明します。- これが
_(ワイルドカード) を強制する理由です。 -
enumに#[non_exhaustive]属性がついている場合、将来バリアントが増える可能性があるため、たとえ全バリアントを書いても_が必須になります。
- これが
-
B. コンパイル後の姿(MIR: Mid-level IR)
matchは、MIRの段階で主にSwitchIntという命令に変換されます。- ジャンプテーブル (Jump Table):
マッチ対象が「密な整数(0, 1, 2, 3...)」や「列挙型のDiscriminant」の場合、コンパイラは のジャンプテーブルを生成します。どれだけ分岐が多くても一瞬で飛びます。O(1) - 二分探索 / 決定木 (Binary Search / Decision Tree):
値が疎(0, 100, 5000...)な場合や文字列の場合、一連の if-else チェーンよりも効率的な、比較による決定木( )を生成しようとします。O(\log N) - ビットマスク最適化:
特定のケースでは、ビット演算を用いて複数のパターンを一度にチェックする最適化が行われることもあります。
- ジャンプテーブル (Jump Table):
結論: 一般的に、動的ディスパッチ(dyn Trait)よりも match(静的ディスパッチ + ジャンプテーブル)の方が、CPUの分岐予測とキャッシュ効率の観点から高速です。
5. match の落とし穴:Drop順序とデッドロック
match 式の中で生成された一時変数(Temporaries)の寿命は、以前は少し直感的ではありませんでした(アームの終わりまで生きるか、式の終わりまで生きるか)。
Temporary Scopes
matchの対象式(Scrutinee)で生成された一時的な値(ロックガードなど)は、match 式全体が終了するまでドロップされないことがあります。
use std::sync::Mutex;
let m = Mutex::new(1);
// 注意: lock() が返す MutexGuard は match ブロックが終わるまで解放されない!
match *m.lock().unwrap() {
1 => {
// ここで再度 m.lock() しようとするとデッドロックする可能性がある
// (再帰的なロック取得などの場合)
},
_ => (),
} // ここで MutexGuard が drop される
これはバグではありませんが、if let や match で Mutex を扱う際の有名なハマりポイントです。
6. モダンRustにおける実用的なTips
- A.
matches!マクロ
単に「マッチするかどうか(bool)」だけが知りたい場合、match を書くのは冗長です。
// 冗長
let is_valid = match status {
Status::Ok | Status::Pending => true,
_ => false,
};
// 推奨
let is_valid = matches!(status, Status::Ok | Status::Pending);
- B. インライン const パターン
定数式をパターンとして直接使用できるケースが増えています。
const LIMIT: i32 = 100;
match x {
// 以前はリテラルか定数変数のみだったが、
// インラインでconstブロックを使える場面も(機能フラグや文脈による)
y if y > LIMIT => ...,
_ => ...
}
Discussion