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

Dart 3パターンマッチングの深淵:`if-case` と `switch` 式の「失敗」がもたらす挙動の差異と堅牢な設計

コードレビューをしていて、最もゾッとする瞬間のひとつは、開発者が「パターンマッチングを単なるシュガーシンタックス(糖衣構文)」として捉えているのを見たときだ。

Dart 3で導入されたパターンマッチングは、単にボイルプレートコードを削ぎ落とすためのものではない。コンパイラとDart VMが、実行時における型の安全性と網羅性を厳密に検証するための強力な静的解析の武器である。

特にフロントエンド(Flutter)や非同期API連携を伴うWeb/アプリ開発において、ネットワークから流れてくる動的なJSONや、UIの状態遷移(State)を扱う際、「パターンが一致しなかった(失敗した)ときに制御フローがどう振る舞うか」を完全に理解しているか否かで、プロダクションの堅牢性は天と地ほどの差が出る。

今回は、`if-case` と `switch` 式における「失敗(Failure)」の定義と挙動の決定的な違いを紐解き、バグの温床を断つためのアーキテクチャ設計を伝授する。

—

1. パターンマッチングにおける「失敗(Match Failure)」とは何か?

まず大前提として、Dartのパターンマッチングにおける「失敗」とは、「評価対象の値が、指定されたパターン構造や型、条件(ガード)に合致しないこと」を指す。

例えば、`int x` というパターンはほとんどの値にマッチする(Irrefutable: 反駁不能なパターン)が、特定の定数 `200` や、構造を持つ `Map(entry)`、あるいは型を絞り込む `String s` は、実行時の値によってマッチしたりしなかったりする(Refutable: 反駁可能なパターン)。

この「反駁可能なパターン」が不一致を起こした際、`if-case` と `switch` 式は全く異なるアプローチをとる。ここを曖昧にしていると、意図しないスキップや、最悪の場合はサイレントバグを生む。

—

2. `if-case` の挙動:安全な「素通り(Bypass)」

`if-case` は、条件分岐の拡張だ。基本的には「もしパターンにマッチしたらブロックに入り、マッチしなければ何事もなかったかのようにスルーする」という挙動を示す。

ここで重要なのは、「マッチしなかった場合のフォールスルーやエラーハンドリングが強制されない」という点だ。

// APIレスポンスを模したJSONデータ
final Map response = {‘status’: 400, ‘message’: ‘Bad Request’};

void handleResponse() {
// if-case によるパターンマッチング
if-case response case {‘status’: 200, ‘data’: final String data}:
print(‘Success: $data’);
// マッチしなかった場合、ここへ流れるが…
}

上記のコード、何が問題か分かるか?
ステータスが `400` の場合、パターンは「失敗」する。しかし、`if-case` には `else` が書かれていないため、何も起きずに処理がそのまま次の行へ流れる(サイレント・ドロップ)。

フロントエンドでこれをやると、「ボタンを押したのに何も起きない」「画面がローディング状態のままフリーズしたように見える」という最悪のUXバグに直結する。

堅牢な `if-case` の設計原則

`if-case` を使う場合は、「失敗したときのルート(`else`)」を常に意識するか、早期リターン(Guard Clause)として利用するべきだ。

void handleResponseRobustly(Map response) {
// 失敗時に即座に弾くガード節としての if-case
if (response case !{‘status’: 200}) {
// 200以外はすべてここでトラップする
print(‘Error or invalid status received.’);
return;
}

// ここに到達した時点で 200であることが保証されている
// ※ただし、構造の厳密な網羅性はコンパイル時に保証されない点に注意
}

—

3. `switch` 式の挙動:コンパイラによる「網羅性(Exhaustiveness)の強制」

一方、Dart 3の真骨頂である `switch` 式(Expression) は、制御構文(Statement)ではなく「式」であるため、すべての値の組み合わせを網羅することが言語仕様として強制される。

もし、パターンの「失敗(網羅漏れ)」が存在する場合、Dartのコンパイラ(AOT/JITコンパイラ)は容赦なくコンパイルエラーを吐き出す。

sealed class UIState {}
class Loading extends UIState {}
class Success extends UIState {
final String data;
Success(this.data);
}
class ErrorState extends UIState {
final String message;
ErrorState(this.message);
}

String render(UIState state) {
// switch 式 の場合、全パターンの網羅が強制される
return switch (state) {
Loading() => ‘Loading…’,
Success(data: final d) => ‘Data: $d’,
// ErrorState を書き忘れると、ここでコンパイルエラーになる!
// “The type ‘UIState’ is not exhaustively matched by the switch cases.”
};
}

この「網羅性の強制」こそが、保守性の高いプロダクションコードを書くための最大の武器だ。
将来的に `MaintenanceState` という新しいサブクラスを `sealed class` に追加した場合、Dartコンパイラはコードベース全体から該当の `switch` 式を検出し、「ここに対応漏れがありますよ」と開発者に教えてくれる。これにより、機能追加時のデグレ(デグレッション)をコンパイルタイムで100%防ぐことができる。

—

4. 実務で即応用する:API連携と状態管理の堅牢な設計パターン

では、実際のWebフロントエンドやFlutterのアーキテクチャ(BLiC / Riverpod / Controller層)において、これらをどう使い分けるべきか。

以下の「APIから受け取ったJSONをドメインモデルに安全に変換し、UI状態を決定する処理」を見てほしい。

// — ドメインモデルと状態定義 —
sealed class ApiResult {}
class ApiSuccess extends ApiResult {
final T data;
ApiSuccess(this.data);
}
class ApiFailure extends ApiResult {
final int errorCode;
final String message;
ApiFailure(this.errorCode, this.message);
}

// — リポジトリ層 / ユースケース層の処理 —
ApiResult> parseApiResponse(Object? json) {
// 1. 構造の検証には switch式 もしくは ガード節付き if-case を使う

// JSONがMap型であり、かつ期待するキーを持っているか検証
if (json case Map map) {
switch (map) {
case {‘code’: 200, ‘payload’: final Map p}:
return ApiSuccess(p);
case {‘code’: final int code, ‘message’: final String msg}:
return ApiFailure(code, msg);
default:
return ApiFailure(-1, ‘Unknown response structure’);
}
}

return ApiFailure(-99, ‘Invalid JSON format’);
}

// — UI / プレゼンテーション層での処理 —
String buildUIStateMessage(ApiResult> result) {
// 2. 状態のハンドリングには必ず switch 式 を使い、網羅性を担保する
return switch (result) {
ApiSuccess(data: var d) => ‘取得成功: ${d.length}件のレコード’,
ApiFailure(errorCode: 401, message: _) => ‘認証エラーです。再ログインしてください。’,
ApiFailure(errorCode: var code, message: var msg) => ‘エラー ($code): $msg’,
};
}

この設計の優れている点(テクニカルリードの視点)

1. 二段構えのバリデーション:

  • 不確実な外部入力(JSON)の解析には、柔軟な `if-case` と網羅的な `switch` 式のデフォルト(`default:`)を組み合わせ、不正なデータ構造によるクラッシュを完全に防いでいる。

2. UI層での網羅性保証:

  • `ApiResult` を `sealed` にしているため、将来的に `ApiMaintenance` などの新しい状態が追加された際、`buildUIStateMessage` の `switch` 式がコンパイルエラーを起こし、対応漏れを絶対に許さない構造になっている。

3. Dart VMの最適化への寄与:

  • 厳密な型チェックとパターンマッチングが静的に解決されるため、Dart VM(JIT/AOT)は効率的なジャンプテーブルの生成や型のインライン化を行いやすくなり、実行時オーバーヘッドが最小限に抑えられる。

—

5. まとめ:プロフェッショナルとしての選択基準

パターンマッチングにおける「失敗」の扱いは、コードの品質を測るリトマス試験紙だ。

  • `if-case` は「関門(Gatekeeper)」として使え。
  • 条件に満たないものを早期リターンで弾く、あるいは特定の条件だけを安全に摘出したい場合に使う。ただし、失敗時のフォールスルーに自ら責任を持つこと。
  • `switch` 式は「全路監視(Exhaustive Controller)」として使え。
  • ドメインの状態遷移や、取りうる値が有限なケース(`sealed class` や `enum`)では、必ず `switch` 式を使い、コンパイラの網羅性チェックを味方につけろ。

「動けばいい」という妥協を捨て、Dartの型システムとコンパイラの力を極限まで引き出すこと。それこそが、大規模・長寿命なプロダクトを支えるエンジニアの矜持である。

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