コードレビューの現場から:パターンマッチングの「罠」を見抜く
「おい、このコード、一見すると美しく書かれているが、実行時特性をまったく理解していない。このままマージしたら、高頻度で呼ばれるUIの再描画やイベントハンドラで確実にボトルネックになるぞ」
シニアエンジニアやテクニカルリードとしてコードレビューをしていると、Dart 3で導入されたパターンマッチングの強力さに魅せられ、その内部挙動を無視して危険な記述をしてしまっているプルリクエストに直面することがある。
特に、`switch` 式や `case` 句と組み合わせて使われる ガード句 (`when`) は、その記述の簡潔さゆえに、安易に重い計算処理や非同期的な文脈を匂わせるロジックを埋め込んでしまいがちな魔力を持っている。
今回は、Dartコンパイラがパターンをどのように評価し、ガード句が実行時にどのような順序とコストで処理されるのか、その深層を解き明かそう。フロントエンド(Flutter)や複雑な非同期API連携を伴うWeb/アプリ開発の現場で、パフォーマンスと堅牢性を両立させるための設計思想を伝授する。
—
Dart 3 パターンマッチングとガード句の評価メカニズム
まず、Dart VMとコンパイラの視点に立って、パターンマッチングがどのように評価されるかを再確認する。
Dart 3のパターンマッチングは単なる「多分岐の糖衣syntax(シンタックスシュガー)」ではない。構造的型チェック(Structural Type Checking)、網羅性チェック(Exhaustiveness Checking)、そして値の分解(Destructuring)をコンパイル時に最適化し、効率的なジャンプテーブルや条件分岐ツリーにコンパイルする。
しかし、その中に ガード句 (`when
ガード句は「パターンマッチングの成否を決める最後の関門」ではない
多くの開発者は、ガード句を「パターンが構造的に一致した後に、追加の条件をフィルタリングするためのオプショナルな述語(Predicate)」と捉えている。これは概念的には正しいが、評価の順序とコストを考える上では不十分だ。
Dartの仕様において、ガード句はパターンマッチングの評価フローの途中に割り込む、動的な式(Expression)である。
ここで重要なのは、「ガード句の中身は、構造的マッチが成功した『その瞬間』に、毎回上から順に、遅延なく評価される」という点だ。
もしガード句の中に以下のような処理が含まれていた場合、どうなるだろうか?
- 巨大なリストやマップの走査・集計
- 正規表現のコンパイルとマッチング
- 複雑なオブジェクトの深いプロパティ探索や計算コストの高いバリデーション
これらは、パターンが一致するたびに(あるいは`switch`の評価順序によっては不必要に何度も)実行され、CPUサイクルを無駄に消費する。最悪の場合、UIスレッドをブロックし、フレームドロップ(カクつき)を引き起こす。
—
❌ アンチパターン:ガード句にコストの高いロジックを詰め込むな
実際の開発現場で見かける、危ういコードの構造を見てみよう。APIから受け取った生データをドメインモデルに射影し、状態に応じてハンドリングするシーンを想定する。
// 【アンチパターン】ガード句の中で重い計算や検証を行っている例
sealed class ApiEvent {}
class DataReceived extends ApiEvent {
final List
String processEvent(ApiEvent event) {
return switch (event) {
// 構造だけをマッチさせ、ガード句で「中身の重い集計やパース」を行っている
DataReceived(rawPayload: var data)
when _calculateTotalRiskScore(data) > 80.0 && _hasCriticalErrors(data) =>
‘CRITICAL_ALERT’,
DataReceived(rawPayload: var data)
when _calculateTotalRiskScore(data) > 50.0 =>
‘WARNING’,
_ => ‘NORMAL’,
};
}
// 計算コストの高いモック関数
double _calculateTotalRiskScore(List
bool _hasCriticalErrors(List
なぜこれが最悪なのか?
1. 評価の重複(Redundant Evaluation):
最初の `case` のガード句で `_calculateTotalRiskScore(data)` が実行され、もしスコアが `75.0` だった場合、最初の条件(`> 80.0`)は 不成立 になる。
しかし、Dartは次の `case` に進み、全く同じデータに対して再び `_calculateTotalRiskScore(data)` を実行する。これにより、計算コストが倍増する。
2. 可読性と責務の分離の崩壊:
パターンマッチングの本来の目的は「データの構造を安全に分解し、型と形状に基づいた分岐を行うこと」である。そこにビジネスロジックや計算処理が混入すると、コードの意図が曖昧になる。
—
⭕ プロダクションコード:計算を事前に行い、ガード句を「純粋な述語」に徹させる
では、どう設計すべきか。
答えは明快だ。「重い計算はパターンマッチングに入る前、あるいはデータモデルの構築時に一度だけ行い、ガード句にはプリミティブな比較や真偽値の参照のみを置く」。
さらに、実務で求められる堅牢性を担保するため、不正な状態をコンパイルレベル・設計レベルで弾くクリーンなコードパターンを提示する。
// 【プロダクション・ベストプラクティス】
// 1. 計算済みのメタデータを持つイミュータブルなモデルを定義する
class RiskAssessment {
final double totalScore;
final bool hasCriticalErrors;
const RiskAssessment({
required this.totalScore,
required this.hasCriticalErrors,
});
// ファクトリコンストラクターや外部サービスで1度だけ計算を完結させる
factory RiskAssessment.fromRaw(List
for (final item in rawPayload) {
score += (item[‘score’] as double? ?? 0.0);
if (item[‘status’] == ‘fatal’) {
critical = true;
}
}
return RiskAssessment(totalScore: score, hasCriticalErrors: critical);
}
}
sealed class ApiEvent {}
class AnalyzedDataReceived extends ApiEvent {
final RiskAssessment assessment;
AnalyzedDataReceived(this.assessment);
}
// 2. パターンマッチングでは「計算済みの値」を分解し、ガード句は高速な比較のみにする
String processOptimizedEvent(ApiEvent event) {
return switch (event) {
// ガード句は単なるフラグと数値の比較のみ(数クロックで評価完了)
AnalyzedDataReceived(assessment: var a)
when a.totalScore > 80.0 && a.hasCriticalErrors =>
‘CRITICAL_ALERT’,
AnalyzedDataReceived(assessment: var a)
when a.totalScore > 50.0 =>
‘WARNING’,
_ => ‘NORMAL’,
};
}
この設計が優れている理由
- 計算量のオーダー保証: $O(N)$ の走査はデータ受領時の1回に限定されており、`switch` 式の評価自体は $O(1)$ または単純な条件分岐の連鎖($O(M)$ where $M$ is branches)に落ちる。
- ガード句の純粋性: ガード句内の式が副作用を持たず、かつ高速であるため、Dart VMのJIT/AOTコンパイラによる最適化(インライン展開やレジスタ割当の効率化)の恩恵を最大限に受けられる。
- メンテナンス性: ビジネスロジック(リスク評価のアルゴリズム)が `RiskAssessment` クラスにカプセル化されており、UIやイベントハンドラのレイヤーから完全に切り離されている。
—
匠の知見:Dart 3 パターンマッチングを極めるための3つの掟
フロントエンドや非同期API連携の現場で、明日から使える実践的な指針をまとめる。
1. ガード句 (`when`) は「保険」であって「メインのロジック置き場」ではない
ガード句は、型や構造だけでは表現しきれない「値の境界値条件」をハンドリングするためのものだ。ここに複雑なメソッド呼び出しを書くべきではない。どうしても複雑な条件が必要な場合は、事前にボイラープレートを抜けたプロパティとして保持させよ。
2. `switch` 式の順番(上から順の評価)をハックせよ
Dartの `switch` 式は上から順にマッチングを試みる。発生確率の最も高い(あるいはパフォーマンス上最初に弾くべき)条件を上に置くのは基本だが、ガード句の評価コストが高い場合(避けるべきだが、やむを得ない場合)、「コストの低いガード句を上に、コストの高いガード句を下に」 配置することで、無駄な評価コストの発生をショートサーキット(短絡評価)で防ぐことができる。
3. 非同期処理 (`async/await`) をガード句に持ち込むな
そもそもDartのガード句内では `await` キーワードは使用できない(構文エラーになる)。しかし、非同期的な判定結果を無理やりキャッシュやFutureのまま扱おうとしてガード句を複雑化させる開発者がいる。非同期処理の結果は、必ずイベントやステートの遷移(State Machine)として昇華させ、同期的に解決できるプリミティブな状態としてパターンに持ち込め。
—
結びに代えて
Dart 3のパターンマッチングは、言語の表現力を飛躍的に高めた。しかし、表現力が上がった分だけ、コードの裏側で何が行われているのかを想像する「エンジニアの解像度」が問われるようになった。
「動けばいい」というコードから、「コンパイラとVMがどう解釈し、どう実行するか」を見通した美しいコードへ。
今日のコードレビューから、あなたのチームのプロダクトをワンランク上のレイヤーへと引き上げてほしい。