【Dart 3】パターンマッチングの「ガード句(when)」完全攻略!評価順序とパフォーマンスの罠を先輩が優しく解説
こんにちは!Dart/Flutter開発を楽しんでいますか?
Dart 3で導入された「パターンマッチング(Pattern Matching)」、本当に便利ですよね!複雑なデータ構造をサクッと解体(デストラクタ化)して条件分岐できるようになったことで、コードの可読性が格段に上がりました。
中でも、パターンにさらに細かな条件を付け加えられる「ガード句(`when`)」は、日常の開発で頻繁に使うことになる強力な武器です。
しかし、このガード句には「コンパイラがどういう順番で評価しているか(評価順序)」と、それに伴う「パフォーマンス上の落とし穴」が存在します。
ここを正しく理解していないと、「なんだかアプリの動きが重いな…」「意図しない順番で処理が走ってしまう…」といったトラブルの原因になってしまうのです。
今回は、Dartの内部メカニズム(Dart VMやAOTコンパイラがどう動いているか)の視点も交えつつ、初学者や他言語から来たみなさんが直感的に理解できるように、丁寧に噛み砕いて解説していきますね。
ここをクリアすれば、Dartのパターンマッチングはバッチリマスターできますよ!一緒におさらいしていきましょう!
—
1. そもそも「ガード句(when)」ってなに?
ガード句とは、パターンマッチングにおいて「型の形や構造は合っているけれど、さらに特定の条件を満たしている場合だけ処理したい」という時に使う追加の条件式です。キーワード `when` を使って記述します。
まずはシンプルな例を見てみましょう。
void checkScore(Object data) {
// if-case文でのガード句の例
if (data case int score when score >= 80) {
print(‘素晴らしい!高得点です: $score点’);
} else if (data case int score) {
print(‘スコア: $score点’);
} else {
print(‘スコアデータではありません’);
}
}
void main() {
checkScore(95); // 素晴らしい!高得点です: 95点
checkScore(50); // スコア: 50点
checkScore(‘100’); // スコアデータではありません
}
とても直感的で読みやすいですよね!
`data case int score when score >= 80` は、「`data` が `int` 型であり(パターンマッチ)、なおかつ `score` が 80 以上であること(ガード句)」という意味になります。
—
2. 【最重要】ガード句の「評価順序」はどうなっている?
ガード句を使う上で、絶対に覚えておいてほしい鉄則があります。
それは、「① パターンの構造・型チェックが先」⇒「② ガード句(when)の評価が後」 という厳格な順番で実行されるということです。
言葉だけだとイメージしづらいので、Dart内部での判定フローを図解で見てみましょう!
パターンマッチングの内部判定フロー
[ 入力データ ]
│
▼
┌────────────────────────────────────────┐
│ 1. パターン抽出(構造・型・値の判定) │ ──(不一致)──► 次の case へスルー
└────────────────────────────────────────┘
│ (一致!変数が展開される)
▼
┌────────────────────────────────────────┐
│ 2. ガード句(when 条件式の評価) │ ──( false )──► 次の case へスルー
└────────────────────────────────────────┘
│ ( true ! )
▼
┌────────────────────────────────────────┐
│ 3. 実行ブロック(Body)の処理を実行 │
└────────────────────────────────────────┘
この「順番」を実験コードで確認してみましょう。以下のコードを実行すると、コンソールにどのような順番でログが出力されるでしょうか?
// 評価順序を検証するための実験用コード
bool checkGuard(String name) {
print(‘ [Guard] “$name” のガード句(when)を評価中…’);
return true;
}
void main() {
print(‘— パターンマッチ開始 —‘);
final Object user = (‘Alice’, 25); // (String, int)のレコード型
switch (user) {
// case 1: 型は (int, int) -> 構造が合わないので when は評価されないはず!
case (int a, int b) when checkGuard(‘Case 1’):
print(‘Case 1 にマッチ’);
break;
// case 2: 型は (String, int) -> 構造が一致!その後に when が評価される!
case (String name, int age) when checkGuard(‘Case 2’):
print(‘Case 2 にマッチ: $name ($age歳)’);
break;
default:
print(‘どれにもマッチしませんでした’);
}
}
実行結果:
— パターンマッチ開始 —
[Guard] “Case 2” のガード句(when)を評価中…
Case 2 にマッチ: Alice (25歳)
解説:
ご覧ください!`Case 1` の `checkGuard(‘Case 1’)` は一度も実行されていませんよね。
Dart VMは、まず最初に型や構造が合っているか(`user` が `(int, int)` かどうか)をチェックします。この時点で「型が違う」と判明したため、`Case 1` の `when` 条件式は評価すらされずにスキップされたのです。
そして、構造がピッタリ合った `Case 2` に来て初めて、ガード句の `checkGuard(‘Case 2’)` が呼び出されました。
この「無駄な判定を短絡評価(ショートサーキット)する仕組み」は非常に優れているのですが、「ガード句に重い処理を書いた時」に思わぬ罠となって襲いかかってきます。
—
3. パフォーマンスの罠!ガード句に「重い計算」を入れてはいけない理由
ここからが、エンジニアとして一歩差がつく重要なポイントです!
Dartのパターン判定(型チェックやレコードの分解)は、コンパイル時に最適化され、実行時(AOTコンパイル後)にはメモリのタグチェックやオフセット参照といった極めて高速な機械語レベルの処理に変換されます。
一方で、ガード句(`when` の後ろに書く式)は、実行時に毎回評価される通常のDartコード(関数呼び出しや計算)です。
つまり、以下のようなコードを書くと、パフォーマンス低下の原因になります。
❌ やってしまいがちな失敗例(アンチパターン)
// 非常に重い計算や複雑な処理をする関数(例:巨大テキストの正規表現チェックやデータ変換)
bool isSpecialUser(String id) {
print(‘ ⚠️ 重い計算が発生中… (ID: $id)’);
// 擬似的な重い処理
return id.startsWith(‘VIP’);
}
void processOrder(Object request) {
switch (request) {
// 構造はマッチするが、毎回 `isSpecialUser` が実行されてしまう!
case (String id, double amount) when isSpecialUser(id):
print(‘VIPユーザーの注文処理: $amount円’);
break;
case (String id, double amount):
print(‘一般ユーザーの注文処理: $amount円’);
break;
}
}
void main() {
final order = (‘GUEST_123’, 5000.0);
processOrder(order);
}
実行結果:
⚠️ 重い計算が発生中… (ID: GUEST_123)
一般ユーザーの注文処理: 5000.0円
何が起きているのか?
`order` は `’GUEST_123’` なので、最終的には「一般ユーザー」として処理されます。
しかし、最初の `case` の構造 `(String id, double amount)` にマッチしてしまったため、「VIPかどうかを調べる重い計算(`isSpecialUser`)」がガード句で無駄に実行されてしまったのです!
もし `switch` の選択肢(`case`)が10個あり、それぞれのガード句で重い処理を書いていたら、マッチするまでに何回も重い関数が実行されてアプリがカクついてしまいます。
—
4. プロのベストプラクティス:こう書き直そう!
ガード句によるパフォーマンスの悪化を防ぎ、美しく高速なコードを書くための 3つの鉄則 をご紹介します。
解決策1:重い判定はガード句ではなく「case内部(Body)」へ移動する
ガード句の中で分岐させようとせず、パターンマッチで大枠の構造を素早く捕まえた後、実行ブロック(Body)の中で `if` 文を使って処理するのが最も高速です。
// ⭕️ 改善例:ガード句をシンプルにする
void processOrderOptimized(Object request) {
switch (request) {
case (String id, double amount):
// パターンマッチが成功した後、必要な場合のみ重い処理を行う
if (isSpecialUser(id)) {
print(‘VIPユーザーの注文処理: $amount円’);
} else {
print(‘一般ユーザーの注文処理: $amount円’);
}
break;
}
}
これなら、パターンチェックが一瞬で終わり、無駄な関数の二重実行も防げますね!
—
解決策2:ガード句には「軽量な比較(`>` `
ガード句は、プロパティの数値比較やEnumのチェックなど、CPUコストがほぼゼロの軽量な条件に絞って使うのがDartの理想的なスタイルです。
// ⭕️ 理想的なガード句の使い方
switch (status) {
// 単純な数値比較ならガード句に書いても超高速!
case Temperature(celcius: final c) when c > 35.0:
print(‘猛暑日です’);
break;
case Temperature(celcius: final c) when c < 0.0: print('真冬日です'); break; } ---
解決策3:シールクラス(Sealed Class)を活用してパターン自体で分岐する
Dart 3の最強機能である `sealed class` を使うと、ガード句(`when`)に頼らず、「型そのもの」で完璧に高速分岐させることができます。
// sealed class でユーザー種別を型として定義する
sealed class UserRole {}
class VipUser extends UserRole { final String id; VipUser(this.id); }
class GeneralUser extends UserRole { final String id; GeneralUser(this.id); }
void processUserRole(UserRole role, double amount) {
// コンパイラが「型」だけで一瞬でジャンプテーブルを作成するため、超高速!
switch (role) {
case VipUser(id: final id):
print(‘VIP ($id) の注文: $amount円’);
break;
case GeneralUser(id: final id):
print(‘一般 ($id) の注文: $amount円’);
break;
}
}
これならガード句自体が不要になり、Dart VMのAOTコンパイラが最適化をフルに効かせることができますよ!
—
5. 初学者が陥りやすい「文法エラー」と「注意点」
最後に、ガード句を使う際に初心者がつまずきやすいポイントを2つ整理しておきましょう!
① ガード句で副作用(Side Effect)を起こしてはいけない
ガード句の中で変数を書き換えたり、API通信を呼んだりするような「副作用のあるコード」は絶対に避けましょう。
なぜなら、前述の通り「ガード句が評価されるかどうか、何回評価されるか」はパターンの順序に依存するため、コードの挙動が予想できなくなってしまうからです。
// ❌ 絶対にNG! ガード句の中でカウントアップなどの副作用を起こす
int count = 0;
switch (data) {
case int x when (++count > 5): // パッチマッチの評価に伴って count が増えてしまう!
print(‘5回以上評価された’);
break;
}
② 網羅性チェック(Exhaustiveness Check)への影響
`switch` 式(Expression)を使う際、Dart 3は「すべてのパターンがカバーされているか」をコンパイル時にチェックしてくれます。
しかし、ガード句(`when`)をつけると、コンパイラは「その `case` が絶対に実行されるかどうか」を事前予測できなくなります。
そのため、`default` やワイルドカードパターン(`_`)を書いておかないと、コンパイルエラーになることがあります。
int number = 10;
// ❌ コンパイルエラー! (non-exhaustive switch expression)
// コンパイラは「when number > 0」が false になった場合に行き先がないと判断する
String result = switch (number) {
int n when n > 0 => ‘正の数’,
};
// ⭕️ 修正後:ガード句のないフォールバック(デフォルト)を用意する
String resultFixed = switch (number) {
int n when n > 0 => ‘正の数’,
_ => ‘ゼロまたは負の数’, // これがあるので安心!
};
—
まとめ
今回はDart 3のパターンマッチングにおける「ガード句(`when`)」の評価順序とパフォーマンスへの影響について深掘り解説しました!
大事なポイントを振り返ってみましょう。
1. 評価の順番: 「パターンの型・構造チェック」が先! 合格したものだけが「ガード句(`when`)」の評価に進みます。
2. パフォーマンスの罠: ガード句に重い関数や計算を書くと、無駄な呼び出しが発生してアプリが重くなる原因になります。
3. 解決策: ガード句は「単純な比較」にとどめ、重い処理は `case` 内部(Body)で処理するか、`sealed class` による型分岐を活用しましょう!
Dartのパターンマッチングは、仕組みをしっかり理解して使うと、驚くほど安全で高速なコードが書けるようになります。
今回のポイントを押さえておけば、Dartの条件分岐・パターンマッチングの基礎はバッチリマスター完了です!ぜひ日々のFlutter/Dart開発で試してみてくださいね。
何か分からないことや疑問があれば、いつでも聞いてください!応援しています!