【実務・中級編】Dartの「パターンマッチング」と「従来のif-else」:実行速度のベンチマーク検証 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューの場で、こんなコードを見かけたことはないだろうか。

// よく見るが、Dartのランタイム特性を無視した冗長なコード
Object processResponse(Map json) {
if (json.containsKey(‘status’) && json[‘status’] == 200) {
if (json.containsKey(‘data’) && json[‘data’] is Map) {
return SuccessData(json[‘data’] as Map);
}
} else if (json.containsKey(‘error’)) {
return ErrorData(json[‘error’].toString());
}
return UnknownData();
}

フロントエンド開発やAPI連携において、JSONやステートの網羅的なハンドリングは日常茶飯事だ。しかし、従来の `if-else` や `is` チェックを重ねる手法は、可読性を落とすだけでなく、Dart VMの最適化パスに対しても優しくない。

Dart 3で導入されたパターンマッチング(Pattern Matching)は、単なる「シンタックスシュガー」ではない。コンパイラが構造を静的に解析し、ジャンプテーブルの最適化やスマートキャストの恩恵を最大限に引き出すための強力な武器だ。

今回は、Dartコアコミッターの視点から、パターンマッチングが内部でどう評価され、従来の `if-else` と何が違うのか、その実行パフォーマンスと堅牢な設計について徹底的に解説しよう。

—

1. なぜ `if-else` は遅く、パターンマッチングは速いのか?

まず、VMとコンパイラの裏側の話をしよう。

従来の `if-else` や `Map.containsKey`、そして頻繁な型キャスト(`as`)を伴うコードは、実行時に以下のようなコストを支払う。
1. ハッシュルックアップの多発: `containsKey` やキーアクセスは、内部でハッシュ計算と衝突解決のオーバーヘッドを生む。
2. 型ガードの重複: 複数の `is` チェックは、AOT(Ahead-Of-Time)コンパイル時において、効率的な分岐命令(`tableswitch` や `lookupswitch`)へのインライン化を阻害しやすい。

対して、Dart 3の `switch` 式とパターンマッチングは、コンパイラが「網羅性(Exhaustiveness)」を静的に検証できる。
つまり、DartのCFE(Common Front End)とAOTコンパイラは、パターンの構造を解析し、定数時間に近いジャンプ最適化、あるいはオブジェクトのメモリレイアウトに基づいた効率的な型ディスパッチコードを生成する。

ベンチマークをとれば一目瞭然だが、複雑なJSONパースや状態分岐において、パターンマッチングを用いたコードは、冗長な条件分岐に比べて最大で数倍の分岐スループットを叩き出すケースがある(特に型チェックが絡む深層構造において顕著だ)。

—

2. 【プロダクションコード】堅牢で美しいAPIレスポンス・ハンドラー

実務の現場で即座に使える、APIレスポンスのデシリアライズと状態管理を統合したパターンマッチングの実装例を見てみよう。

ここで紹介するのは、コンパイル時の網羅性チェック(Exhaustiveness checking)を強制し、バグの入り込む余地を完全に断つ設計だ。

import ‘dart:core’;

// — ドメインモデルの定義 —
sealed class ApiResponse {
const ApiResponse();
}

class Success extends ApiResponse {
final T data;
final int code;
const Success(this.data, this.code);
}

class ClientError extends ApiResponse {
final int statusCode;
final String message;
const ClientError(this.statusCode, this.message);
}

class ServerError extends ApiResponse {
final String reason;
const ServerError(this.reason);
}

class Maintenance extends ApiResponse {
const Maintenance();
}

// — パターンマッチングを活用した堅牢なハンドラー —
// 戻り値の型を統一し、UIコンポーネントや状態管理(Riverpod/Blocなど)へシームレスに渡す
R handleApiResponse({
required ApiResponse response,
required R Function(T data) onSuccess,
required R Function(int code, String message) onError,
required R Function() onFallback,
}) {
// Dart 3の switch式 と オブジェクトパターン の組み合わせ
// コンパイラが全サブクラスの網羅を強制するため、将来ステートが増えてもハンドリング漏れが起きない
return switch (response) {
// 1. Successパターンの構造化束縛 (Destructuring)
Success(data: final d, code: >= 200 && < 300) => onSuccess(d),

// 2. ガード節付きパターンマッチング (HTTP 4xx系)
ClientError(statusCode: final code, message: final msg) => onError(code, msg),

// 3. サーバーエラーおよびメンテナンスを包括的にハンドリング
ServerError(reason: final r) => onError(500, ‘Server Error: $r’),
Maintenance() => onError(503, ‘System is under maintenance’),

// 4. ワイルドカードを用いた網羅性担保(通常はsealedにより到達不能だが安全網として)
_ => onFallback(),
};
}

// — 実行テストと出力例 —
void main() {
// モックAPIレスポンス
final List>> responses = [
const Success({‘userId’: 42, ‘name’: ‘Dart Architect’}, 200),
const ClientError(404, ‘Not Found’),
const ServerError(‘Database connection timeout’),
const Maintenance(),
];

for (final res in responses) {
final resultMessage = handleApiResponse(
response: res,
onSuccess: (data) => ‘【成功】ユーザー名: ${data[‘name’]} (ID: ${data[‘userId’]})’,
onError: (code, msg) => ‘【エラー [$code]】 $msg’,
onFallback: () => ‘【警告】予期せぬレスポンスです’,
);

print(resultMessage);
}
}

このコードが美しい理由(コードレビューの視点)

1. `sealed class` とのシナジー:
`ApiResponse` を `sealed` にすることで、同一ライブラリ外からの勝手なサブクラス化を防ぎつつ、`switch` 式での網羅性チェックをDartコンパイラ(CFE)に強制させている。新しいステート(例: `TokenExpired`)を追加した瞬間、コンパイルエラーになるため、「ハンドリングし忘れて本番でクラッシュする」というバグが物理的に不可能になる。
2. 宣言的なガード節(`code: >= 200 && < 300`):
パターン内部で直接値の範囲を評価できるため、`if (code >= 200 && code < 300)` のようなネストが消滅し、コードの意図が視覚的にもストレートに伝わる。 3. ゼロ・コストに近いスマートキャスト:
パターンマッチが成功した瞬間に変数(`final d`, `final msg`)へ厳密な型でバインドされるため、無駄な `as` キャストが一切不要。

—

3. パフォーマンス上の注意点とアンチパターン

パターンマッチングは万能の魔法ではない。誤った使い方をすると、かえってパフォーマンスを悪化させたり、保守性を損ねたりする。

注意点1: 巨大なJSONの直接パターンマッチング

受け取った生の `Map` に対して、深い階層まで一気にパターンマッチングを適用しようとすると、実行時の型チェックとキー走査コストが膨らむ場合がある。

アンチパターン:

// 巨大なMapから深層のデータを無理やりパターンで抜こうとする
String extractName(Map json) => switch (json) {
{‘user’: {‘profile’: {‘name’: String n}}} => n,
_ => ‘Guest’,
};

なぜ非効率か: Dartの `Map` パターンは安全だが、ネストが深くなるとコードの可読性が落ち、実行時のMapルックアップが連続する。

推奨されるプラクティス:
一度、型安全なデータクラス(DTO)や `record` にデシリアライズしてから、そのドメインモデルに対してパターンマッチングを適用するべきだ。境界(Network Boundary)でのみパースコストを払い、ビジネスロジック層では純粋なオブジェクト構造に対してパターンを使うこと。

—

4. まとめ:コードを「語らせる」ために

Dart 3のパターンマッチングは、単にコード行数を削るための機能ではない。
「コンピュータが理解しやすい構造」と「人間がバグを見つけやすい構造」を一致させるためのコンパイラ機能である。

これまでの泥臭い `if (x != null && x is Map && …)` という防御的コードの山から脱却し、宣言的でコンパイラに守られた堅牢なコードベースへ移行しよう。

あなたの書くコードは、ただ動くだけではなく、Dart VMの最適化エンジンと、コードレビューをする未来のチームメイトに対して、最高にリスペクトのあるものであろうか?
今日からその `if-else` を `switch` 式に書き換えてみせよう。

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