【実務・中級編】Dart 3のパターンマッチングにおける「失敗」の伝播:if-caseとswitch式の挙動の違い – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart 3パターンマッチングの深淵:`if-case` と `switch` 式における「失敗の伝播」を極める

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

// よく見かけるが、実は冗長かつ保守性を下げるコード
String getStatusLabel(Map json) {
if (json.containsKey(‘status’) && json[‘status’] is String) {
final status = json[‘status’] as String;
if (status == ‘active’) return ‘有効’;
if (status == ‘pending’) return ‘保留’;
}
return ‘不明’;
}

Dart 3で導入されたパターンマッチングを使いこなせていない現場では、未だにこのような防衛的コードが散見される。Dartのパターンマッチングは、単なる「見た目のシュガーシンタックス(糖衣構文)」ではない。コンパイラレベルで型の絞り込み(Type Promotion)と「マッチングの失敗(Failure)」を厳密に制御するための強力なメカニズムだ。

今回は、Dartコアの挙動を知り尽くしたアーキテクトの視点から、`if-case` と `switch` 式における「失敗の伝播」のメカニズムを解剖し、フロントエンドやAPI連携の現場で絶対に破綻しない堅牢な設計パターンを伝授する。

—

1. パターンマッチングにおける「失敗(Irrefutable vs Refutable)」の概念

まず、Dart VMがコンパイル時に何を評価しているのかを理解しよう。パターンには大きく分けて2種類ある。

1. 反駁不能なパターン(Irrefutable Patterns):
型推論や変数宣言(`var (a, b) = (1, 2)` など)のように、絶対に失敗しないパターン。
2. 反駁可能なパターン(Refutable Patterns):
定数パターン、リレーショナルパターン、オブジェクトの構造化パターンなど、「一致しない可能性がある」パターン。

`if-case` や `switch` 式で扱うのは、すべて後者の反駁可能なパターンだ。
パターンが一致しなかったとき、Dartの制御フローはどのように挙動するのか? ここに `if-case` と `switch` 式の決定的な違いがある。

—

2. `if-case` の挙動:スコープの限定と「ガード条件」の罠

`if-case` は、条件分岐の中で一時的にパターンマッチを行いたい場合に使う。ここで重要なのは、「マッチしなかった場合の制御フローの継続」である。

void processApiResponse(Map response) {
// responseが特定の構造を持っているか検証しつつバインドする
if-case response case {‘code’: 200, ‘data’: {‘items’: List items}}] {
// ここでは items は List 型にプロモーションされ、安全に扱える
print(‘Loaded ${items.length} items.’);
return;
}

// マッチしなかった場合、処理はここで「中断されずに」次に流れる
// あるいは早期リターンを書き忘れると、意図しないフォールスルーを引き起こす
print(‘Invalid response format.’);
}

チーフアーキテクトからの警告:`if-case` のガード条件 (`when`) の評価順序

`if-case` では `when` キーワードを使ってガード条件を追加できるが、ここにはVMの最適化を阻害する落とし穴がある。

if-case data case [var head, …var tail] when tail.isNotEmpty {
// 処理
}

パターンマッチの評価と `when` の評価は直列に行われる。もし `when` の中で重い計算や非同期に近いコストの処理を行うと、JIT/AOTコンパイラによるインライン化の恩恵を受けられなくなる。ガード条件は、あくまで「パターンの追加の絞り込み(プリミティブな比較)」にとどめるべきだ。

—

3. `switch` 式の挙動:網羅性(Exhaustiveness)の強制と失敗の封じ込め

一方、`switch` 式(StatementではなくExpression)は、Dart 3の真骨頂である。
switch式は「すべての可能なパターンを網羅しなければならない(Exhaustiveness checking)」という静的解析上の厳格な制約を持つ。

もし網羅性が担保されていない場合、コンパイルエラーになる。つまり、「マッチしなかった場合の失敗」をコンパイル時に完全にゼロにすることができる。

// 状態管理やAPIのステータスハンドリングの理想形
sealed class ApiState {}
class ApiLoading extends ApiState {}
class ApiSuccess extends ApiState {
final T data;
ApiSuccess(this.data);
}
class ApiError extends ApiState {
final String message;
ApiError(this.message);
}

String renderUI(ApiState state) => switch (state) {
ApiLoading() => ‘ローディング中…’,
ApiSuccess(data: final d) => ‘データ: $d’,
// ApiError を書き忘れると、この時点でコンパイルエラーになる!
ApiError(message: final msg) => ‘エラーが発生しました: $msg’,
};

ここで重要なのは、`switch` 文(Statement)ではなく、`switch` 式(Expression)を使っている点だ。式であるため、必ず値を返し、網羅性が強制される。これにより、将来的に `ApiState` に新しいサブクラス(例: `ApiTimeout`)が追加された瞬間、アプリ全体で対応漏れのバグをコンパイル時に検知できる。

—

4. 【実践】プロダクションコード:堅牢なAPIレスポンス・パーサーの実装

WebフロントエンドやBFF層との通信において、JSONの構造崩壊や予期せぬデータ型によるクラッシュは最大の敵である。
Dart 3のパターンマッチングを駆使し、「失敗の伝播」を安全にハンドリングするプロダクションコードを提示する。

このコードはそのままコピー&ペーストして、実務のモデル層やリポジトリ層で応用できる。

import ‘dart:convert’;

/// アプリケーション層で扱うドメインモデル
sealed class UserProfileResult {
const UserProfileResult();
}

class UserProfileLoaded extends UserProfileResult {
final String id;
final String name;
final int age;
const UserProfileLoaded({required this.id, required this.name, required this.age});
}

class UserProfileGuest extends UserProfileResult {
const UserProfileGuest();
}

class UserProfileParseError extends UserProfileResult {
final String reason;
const UserProfileParseError(this.reason);
}

/// 堅牢なパース処理クラス
class UserProfileParser {
/// 生のJSON文字列を受け取り、網羅的かつ安全にドメインモデルへ変換する
static UserProfileResult parse(String rawJson) {
try {
final dynamic decoded = jsonDecode(rawJson);

// 1. トップレベルがMapであることの検証とパターンマッチング
return switch (decoded) {
// 正常系パターン1: 登録ユーザー
{
‘status’: ‘success’,
‘data’: {
‘id’: String id,
‘attributes’: {
‘name’: String name,
‘age’: int age,
}
}
} => UserProfileLoaded(id: id, name: name, age: age),

// 正常系パターン2: ゲストユーザー(ageがnullまたは未定義のケースを許容)
{
‘status’: ‘guest’,
} => const UserProfileGuest(),

// 準正常系: ステータスは分かるがデータ構造が期待値と異なる場合
{‘status’: String s} => UserProfileParseError(‘予期せぬステータスまたは構造です: $s’),

// 失敗系: JSONのルートがオブジェクトではない、あるいは必須キーが欠損
_ => const UserProfileParseError(‘未知のJSON構造です。’),
};
} on FormatException catch (e) {
// JSON構文エラーのキャッチ
return UserProfileParseError(‘JSONパース失敗: ${e.message}’);
} catch (e) {
return UserProfileParseError(‘予期せぬエラー: $e’);
}
}
}

// === 実行確認用のmain関数 ===
void main() {
// テストケース1: 正常な登録ユーザーJSON
const json1 = ‘{“status”: “success”, “data”: {“id”: “usr_001”, “attributes”: {“name”: “Alice”, “age”: 28}}}’;

// テストケース2: ゲストJSON
const json2 = ‘{“status”: “guest”}’;

// テストケース3: 壊れたJSON(ageが文字列になっているなど)
const json3 = ‘{“status”: “success”, “data”: {“id”: “usr_002”, “attributes”: {“name”: “Bob”, “age”: “twenty”}}}’;

for (var jsonStr in [json1, json2, json3]) {
final result = UserProfileParser.parse(jsonStr);

// ここでも switch 式を用いて安全に結果を描画・処理する
String message = switch (result) {
UserProfileLoaded(name: var n, age: var a) => ‘ようこそ、$n さん ($a歳)’,
UserProfileGuest() => ‘ゲストとして接続中’,
UserProfileParseError(reason: var r) => ‘⚠️ エラーハンドリング: $r’,
};

print(message);
}
}

この設計が優れている理由(アーキテクトの解説)

1. フォールスルーの排除:
`switch` 式を採用しているため、JSONの構造が変わった際に「どのパターンにも一致しないケース(`_`)」を強制的に意識させられる。これにより、暗黙の `null` や予期せぬ例外の伝播を防ぎ、必ず `UserProfileResult` のいずれかのサブクラスに着地する。
2. 型安全なアンボックス(Unboxing):
`{‘attributes’: {‘name’: String name, ‘age’: int age}}` の部分で、マッチした瞬間にローカル変数 `name` は `String`、`age` は `int` として厳密に型付けられる。従来の `as String` のような危険なキャストコードを完全に駆逐している。
3. エラーの局所化:
パース中の失敗(構造不一致や型違い)は、クラッシュさせるのではなく、専用の `UserProfileParseError` ドメインモデルとしてラップされ、呼び出し元へ安全に伝播(Return)される。これによりUIスレッドが予期せず落ちる(Red Screen of Death)リスクを根絶できる。

—

5. パフォーマンスとVM最適化の観点

「パターンマッチングを多用すると、実行時パフォーマンスに影響はあるのか?」という疑問を持つシニアエンジニアもいるだろう。

Dart VMは、パターンマッチングの構造を最適化し、内部的に効率的なジャンプテーブルや型チェックのインラインキャッシュを生成する。
ただし、巨大なネスト構造を持つJSONや、深い階層のパターンマッチを毎フレーム(60fps/120fpsのUI描画ループ内など)で大量に行うのは避けるべきだ。

  • 推奨アプローチ:

UIコンポーネントのビルドメソッド内で複雑なパターンマッチを行うのではなく、データがレイヤーを跨ぐ境界(Repository層、BFFからのデシリアライズ層)で1度だけパターンマッチを適用し、検証済みの不変(Immutable)なドメインオブジェクトに変換すること。

—

まとめ:失敗を制御する者がコードを制す

Dart 3のパターンマッチングは、単なるシンタックスの化粧直しではない。
「プログラムが失敗しうるポイント」をコンパイラの視界に強制的に捉えさせ、安全な処理経路へとルーティングするための強力な武器である。

`if-case` は局所的な条件付きスコープの拡張に留め、ビジネスロジックの分岐や状態・データのハンドリングには網羅性が保証された `switch` 式をファーストチョイスとして選ぶこと。この設計思想をチーム全体で共有できれば、あなたのプロダクトから「予期せぬ型エラー」や「ハンドリング漏れのバグ」は完全に姿を消すはずだ。

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