【実務・中級編】Dartの「switch式」における網羅性チェックのメカニズムと、コンパイラの警告を制御する方法 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューをしていて、最もゾッとする瞬間の一つが、`switch`文や`if-case`で「とりあえずの `default:`」が置かれているコードを見たときだ。

「将来、APIのレスポンス型が増えたときに安全だから」という免罪符のもとに書かれた `default:` や `_` は、多くの場合、コンパイラが与えてくれる最大の安全網である「網羅性チェック(Exhaustiveness Checking)」を自ら放棄する自殺行為に他ならない。

Dart 3で導入されたパターンマッチングと `switch` 式は、単なるシンタックスシュガーではない。あれはコンパイラ(CFA: Control Flow Analysis / 制御フロー解析)に型空間のすべての可能性を証明させるための数学的装置である。

今回は、Dartコンパイラの内部で網羅性がどう判定されているのか、そして「あえて一部のケースを無視したい」という実務のジレンマにどう立ち向かうべきか、チーフアーキテクトの視点からロジカルに解説しよう。

—

1. コンパイラは「網羅性」をどう証明しているのか?

DartのAOTコンパイラやDevCompiler(DDC)は、`switch` 式に直面したとき、入力される値の「静的型(Static Type)」が持つ値の空間(Domain)を完全に分解し、すべての分岐パターンが網羅されているかをコンパイル時に検証する。

例えば、以下のような `sealed` クラス(または代数的データ型: ADT)を考えてみよ。

sealed class ApiResult {}
class Success extends ApiResult { final T data; Success(this.data); }
class Error extends ApiResult { final Object error; Error(this.error); }
class Loading extends ApiResult {}

この `ApiResult` を受け取る `switch` 式を書くとき、Dartコンパイラは `Success`, `Error`, `Loading` の3つのサブタイプがすべてカバーされているかをチェックする。もし `Loading` が漏れていれば、コンパイルエラー(`The type ‘ApiResult‘ is not exhaustively matched by the switch cases.`)を吐く。

これが意味するのは、「将来、ドメインモデル(サブタイプ)を追加した際、その変更に追随してコードを修正すべき箇所をコンパイラが100%特定してくれる」という強烈な保守性の担保だ。

ここに `default:` や `_` を挟むと、この静的検証の連鎖がプツリと途切れる。新しいサブタイプを追加してもコンパイラは何も教えてくれず、実行時になって初めて予期せぬ挙動(あるいは暗黙の `null` 返却など)に直面することになる。

—

2. 実務のジレンマ:すべてのケースをハンドリングしたくない時

しかし、実務の現場では「コンパイラの網羅性チェックは受けたいが、特定のケースはどうでもいい(あるいは同じ処理にまとめたい)」という状況が発生する。

例えば、UIコンポーネントの状態管理(BloCやRiverpodなど)で、10種類ある画面遷移ステートのうち、9つは「何もしない(あるいは共通のフォールバック)」で、残り1つだけ特殊な処理をしたい場合だ。

ここで `default` を使いたくなる誘惑に駆られるが、それをやると前述の通り「新しいステートが追加されたときのコンパイル時検知」という恩恵を失う。

では、どう設計すべきか?
ここで、Dart 3のパターンマッチングの表現力を極限まで活かした「明示的網羅性とワイルドカードの戦略的分離パターン」を提示しよう。

—

3. 実装例:プロダクションコードにおける堅牢な設計パターン

以下のコードは、Webフロントエンド(Flutter Web等)のAPI連携やコンポーネント描画において、網羅性を維持しつつ冗長なコードを排除した、保守性の高いプロダクションコードの模範解答だ。

import ‘package:flutter/foundation.dart’;

// 1. ドメインモデルの定義 (sealed classによる型の厳格化)
sealed class UiState {}
class UiInitial extends UiState {}
class UiLoading extends UiState {}
class UiSuccess extends UiState { final String payload; UiSuccess(this.payload); }
class UiNetworkError extends UiState { final int statusCode; UiNetworkError(this.statusCode); }
class UiMaintenanceError extends UiState {}
class UiTimeoutError extends UiState {}

/// 2. 堅牢なステートハンドラー
/// 意図的に「default」を使わず、すべての型を明示させることで、
/// 将来新しいエラー型が追加された際にコンパイルエラーで気付けるようにする。
String resolveUserMessage(UiState state) {
return switch (state) {
// 正常系
UiInitial() => ‘準備中…’,
UiLoading() => ‘読み込み中…’,
UiSuccess(:final payload) => ‘データ取得成功: $payload’,

// 異常系:ネットワーク関連は共通化しつつ、型安全性を維持
UiNetworkError(:final statusCode) when statusCode >= 500 => ‘サーバーエラーが発生しました’,
UiNetworkError() => ‘通信環境を確認してください’,

// 【重要】メンテナンスとタイムアウトは「その他エラー」として網羅しつつも、
// default で隠蔽せず、明示的にケースとして列挙している点に注目。
// これにより、例えば新しく `UiAuthError` が追加された瞬間にコンパイルが止まる。
UiMaintenanceError() || UiTimeoutError() => ‘一時的な接続エラーです。しばらくお待ちください’,
};
}

void main() {
// 実行例
final states = [
UiInitial(),
UiLoading(),
UiSuccess(‘Dart 3 究極の知見’),
UiNetworkError(503),
UiMaintenanceError(),
];

for (final state in states) {
if (kDebugMode) {
print(‘State: ${state.runtimeType} -> Message: ${resolveUserMessage(state)}’);
}
}
}

—

4. この設計が優れている理由(コードレビューの視点から)

1. 論理結合演算子 (`||`) の活用によるDRY原則
`UiMaintenanceError() || UiTimeoutError()` のように、複数のパターンを論理ORで結合することで、「網羅性の義務(コンパイラを通すこと)」を果たしながら、「コードの重複(DRY)」を排除している。`default` に逃げない美しい妥協点だ。
2. ガード句 (`when`) による詳細な分岐
`UiNetworkError(:final statusCode) when statusCode >= 500` のように、型の一致だけでなく値の条件まで含めて網羅性チェックの対象にできる。
3. 将来の拡張性に対する免疫
もし明日、プロダクトマネージャーから「やっぱり強制アップデートのエラー画面(`UiForceUpdateError`)を追加して」と言われたとする。このコードは、コンパイル時に容赦なくエラーを吐き、開発者に対処を強制する。バグが本番環境に到達する余地をコンパイラレベルで根絶しているのだ。

—

5. パフォーマンスとVMの最適化に関する注意点

最後に、Dart VMの実行パフォーマンスについて一言添えておく。

Dart 3の `switch` 式は、従来の `switch` 文とは異なり、単なる `if-else` の連続にコンパイルされるとは限らない。取りうる値の性質(整数の連続性、型の階層構造など)に応じて、VMはジャンプテーブル(Jump Table)やインラインキャッシュ、型ガードの最適化を適用する。

特に `sealed` クラスに対する `switch` 式は、コンパイル時にサブタイプの数が完全に確定(Closed World Assumptionに近い状態)するため、仮想メソッド呼び出し(vtable lookup)を伴う `noSuchMethod` や不要なダウンキャストをコンパイラが完全に排除し、極めて高速なネイティブコードにコンパイルされる。

だからこそ、`default:` や `_` を乱用して「何が来るかわからない状態」を意図せず作り出すことは、コンパイラの最適化パスの足を引っ張る行為にもなり得るのだ。

まとめ

コードは書いた瞬間からレガシーへの道を歩み始める。しかし、Dartの強力な型システムと網羅性チェックを正しく使いこなせば、「コードの変更にコンパイラが追従し、デグレを物理的に不可能にする」という、極めてモダンで持続可能な開発体験を手に入れることができる。

明日のコードレビューからは、「なぜそこに `default` があるのか?」を問い直そう。安全装置を外すな。コンパイラに仕事をさせろ。それが、プロフェッショナルのDartエンジニアリングだ。

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