【実務・中級編】Dartのパターンマッチングにおける「ガード句」の評価順序とパフォーマンス – Dart コア文法・オブジェクト指向・Null安全解析バイブル

フロントエンド開発、複雑なコンポーネントの状態管理、そして非同期API連携の渦中において、コードの「堅牢性」と「予測可能性」はエンジニアの生命線だ。

コードレビューをしていて、最近よく見かける光景がある。Dart 3で導入されたパターンマッチング (`switch` 式や `case` 句) を使いこなし、「お、モダンに書けてるね」と思いきや、その中の `when` 節(ガード句)の書き方が雑なために、ランタイムで無駄なコストを支払っていたり、予期せぬバグの温床を作っていたりするケースだ。

今回は、Dartのパターンマッチングにおけるガード句(`when`)の評価順序とパフォーマンス、そして実務で絶対に破綻しない堅牢な設計パターンについて、Dart VMの挙動まで踏み込んでロジカルに解説しよう。

—

1. ガード句の裏側:Dart VMは `when` をどう評価しているか?

まず、コンパイラとランタイムの視点に立とう。
パターンマッチングにおける `case` は、構造の型や形状(Shape)を検証するためのものだ。一方で `when` 続くガード句は、構造が一致した後に評価される任意のブール式である。

ここで重要な事実がある。
Dartの `switch` 式における各 `case` の評価は上から順に行われるが、構造マッチとガード句の評価は「短絡評価」の連鎖として最適化される。

しかし、書き手側がこの評価順序を意識していないと、次のような悪夢が起きる。
1. 重い計算や非同期に近い同期的コストの高い関数がガード句に置かれる。
2. 評価されるべきではないタイミングで評価され、NullPointerException(Null安全文脈では `TypeError` や予期せぬ `false`)を引き起こす。

非同期APIのレスポンスや、刻々と状態が変わるUIのViewModelを扱うフロントエンドエンジニアにとって、この「評価順序の制御ミス」は、デバッグが極めて困難なUIのチラつきや状態不整合の直因となる。

—

2. 非効率なコードの典型例と、その何が問題か

以下のコードを見てほしい。APIから受け取ったドメインモデルのステータスを、UIの表示用コンポーネント向けにパースする処理だとする。

// 【アンチパターン】パフォーマンスと安全性を軽視したガード句
String resolveBadgeLabel(ApiResponse response) {
return switch (response) {
// パターン1: 何気なく重い関数やプロパティアクセスをwhen句に書いている
(== 200, var data) when _isDataFullyCached(data) && data[‘isVip’] == true
=> ‘VIP Cache Ready’,

// パターン2: 評価順序の罠(dataがnullの可能性をガードの配置で踏み抜く)
(== 200, var data) when data[‘items’].isNotEmpty && data != null
=> ‘Success with Items’,

_ => ‘Standard’,
};
}

このコードのどこがプロの仕事として許されないか、お分かりだろうか。

1. ガード句内の評価順序の崩壊: パターン2では、`data != null` でヌルチェックをしているつもりなのに、その手前の `data[‘items’]` ですでにランタイムエラー(あるいはNull安全文脈での型不一致)が起きる可能性がある。Dartの `&&` は左から評価されるため、構造マッチで `var data` が `dynamic` や `Map?` であった場合、`data` が `null` であっても手前が爆発する。
2. 無駄なコストの支払い: ガード句に書かれた処理は、構造マッチが成功するたびに評価される。ここに副作用のある処理や計算コストの高いバリデーションを置くと、AOTコンパイルされたバイナリであっても無駄なサイクルを消費する。

—

3. 実務で即戦力となる「堅牢かつ高速」なパターンマッチング設計

では、どのように記述すべきか。
結論から言えば、「構造の絞り込み(Pattern)」を左側に寄せ、コストの低い条件、そして安全な順序でガード句(`when`)を構築すること。

以下のプロダクションコードを見てほしい。これは、API連携、ローカルキャッシュ、UIの状態管理が複雑に絡み合うコンポーネント層でそのまま使える設計パターンだ。

import ‘dart:ui’;

// — ドメインモデルの定義 —
sealed class ApiResult {}

class ApiSuccess extends ApiResult {
final Map? payload;
final DateTime fetchedAt;
ApiSuccess(this.payload, this.fetchedAt);
}

class ApiError extends ApiResult {
final int code;
final String message;
ApiError(this.code, this.message);
}

class ApiLoading extends ApiResult {}

// — プロダクション品質のビューロジック —
String resolveUiStateMessage(ApiResult result, {required bool isOfflineMode}) {
return switch (result) {
// 1. 状態の型と構造を厳密に絞り込む
ApiLoading() => ‘読み込み中…’,

// 2. エラー系:コードに応じた分岐。ガード句はシンプルかつ安全に
ApiError(code: >= 500 && < 600) when !isOfflineMode => ‘サーバー側で致命的なエラーが発生しました’,

ApiError(code: 401)
=> ‘セッションが切れました。再ログインしてください’,

// 3. 成功系:【重要】安全な順序とショートサーキットを意識したガード句
// まず payload が null でないことをパターン側、もしくはガードの最左翼で確実に担保する
ApiSuccess(payload: != null)
when result.payload![‘status’] == ‘active’ &&
_isFreshData(result.fetchedAt)
=> ‘アクティブな最新データです’,

ApiSuccess(payload: != null)
when result.payload![‘status’] == ‘active’
=> ‘アクティブですが、キャッシュの有効期限が切れています’,

// 4. フォールバック
ApiSuccess() => ‘データ構造が不正、または非アクティブです’,

_ => ‘不明な状態です’,
};
}

// タイムスタンプの鮮度をチェックする軽量なヘルパー
bool _isFreshData(DateTime fetchedAt) {
return DateTime.now().difference(fetchedAt).inMinutes < 5; }

このコードが優れている理由(テクニカルリードの視点)

1. 密封クラス(`sealed class`)と網羅性(Exhaustiveness)の活用:
`ApiResult` を `sealed` にしているため、将来新しいレスポンス型(例: `ApiMaintenance`)が追加された際、コンパイラが `switch` の網羅性エラーを吐き、デベロッパーのビルドを強制的に止めてくれる。バグの温床をコンパイル時になくす鉄則だ。
2. ガード句に至る前の構造安全性の確保:
`ApiSuccess(payload: != null)` と書くことで、ガード句(`when`)に入る時点で `payload` が非nullであることが保証される。これにより、ガード句内での無駄な `null` チェックや、それに起因するランタイムクラッシュを構造的に排除している。
3. 評価コストの最適化:
よりコストが低い条件(例: `!isOfflineMode` や、単純なマップのキーアクセス)を先に評価し、関数呼び出しや重い判定は適切に分離・順序付けされている。

—

4. パフォーマンス上の注意点:ガード句に「重い処理」を持ち込まない

フロントエンドやFlutterのビルドメソッド (`build`) の中で `switch` 式を使う場合、これが毎フレーム、あるいは状態変更のたびに高頻度で実行されることを忘れてはならない。

  • NG: ガード句の中で複雑な正規表現マッチ、深いJSONの走査、重いコレクション操作(`.where().toList().any()` など)を行うこと。
  • OK: ガード句で行うのは、プリミティブな値の比較、すでに出し分け済みのフラグの評価、あるいは計算済みプロパティの参照に留めること。

複雑なビジネスロジックによる判定が必要な場合は、ガード句に直接書くのではなく、ドメインモデル側にあらかじめゲッター(Getter)としてキャッシュまたは算出させておき、ガード句からはその軽量なゲッターを叩くように設計すべきだ。

// 良い設計:ロジックはモデル側にカプセル化し、ガード句はスッキリ保つ
class User {
final Map rawData;
User(this.rawData);

// 事前に算出・カプセル化されたプロパティ
bool get hasValidPermissions => rawData[‘permissions’]?.isNotEmpty ?? false;
}

// 呼び出し側(スイッチ式)
String evaluateUserAccess(User user) => switch (user) {
_ when user.hasValidPermissions => ‘許可’,
_ => ‘拒否’,
};

—

結びにかえて

Dart 3のパターンマッチングとガード句は、適切に使えばコードベースから膨大な `if-else` の迷宮を消し去り、圧倒的な読みやすさと堅牢性をもたらす強力な武器だ。

しかし、その「便利さ」の裏側にある評価順序のメカニズムとランタイムコストを理解していないと、パフォーマンスの低下や、静的解析をすり抜けるバグを量産する諸刃の剣ともなる。

今日のコードレビューから、チームメンバーの書く `when` 句の「順序」と「重さ」に目を光らせてほしい。そこを統制できたプロジェクトのコードベースは、美しく、そして驚くほど軽快に動き続けるはずだ。

タイトルとURLをコピーしました