【実務・中級編】Dart 3のswitch式における網羅性チェックを「あえて」回避する設計の是非 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart 3の網羅性チェックを「あえて」回避する:堅牢なアーキテクチャのための戦略的判断

Dart 3で導入されたパターンマッチングとswitch式は、我々に「型安全性の極致」をもたらしました。コンパイラが全てのケースを網羅しているか監視し、漏れがあれば即座にビルドを止める。これは大規模開発において強力な武器です。

しかし、現場でコードを叩いていると「あえて網羅性を崩したい」瞬間が訪れます。例えば、外部APIから降ってくるJSONの拡張性や、サードパーティライブラリの列挙型アップデートへの追従です。

本稿では、この「網羅性チェック」という安全装置をいかにして戦略的に外すか、そしてその代償をどう制御するか、アーキテクトの視点から解説します。

—

網羅性チェックを「回避したくなる」のはどんな時か?

多くのエンジニアが直面するのは、「未来の変更」と「現在の堅牢性」のジレンマです。

  • APIの隠れた変更: バックエンドが将来的に新しいステータスコードを追加する際、クライアント側で完全な網羅性を強制していると、そのフィールドを参照する全てのswitch式でコンパイルエラーが発生します。
  • 「その他」を許容する設計: UIコンポーネントにおいて、未知の状態は「とりあえずデフォルト値でレンダリングする」という仕様が許されるケース。

ここで安易に `default` を置くのは、Dart 3の恩恵を自ら捨てる行為です。では、どうすべきか。

—

戦略的回避策: `sealed` クラスと「未知のケース」の明示

網羅性を回避するために `default` を使うのは悪手です。なぜなら、将来的に新しいケースが追加された際、コンパイラが「新しいケースが増えたよ」と警告してくれなくなるからです。

代わりに、「未知のケース」を表す型を明示的に定義するのがアーキテクチャ上の正解です。

実践コード:保守性を損なわない拡張設計

sealed class RemoteState {
const RemoteState();
}

class Loading extends RemoteState {}
class Success extends RemoteState { final String data; Success(this.data); }
class Error extends RemoteState { final String message; Error(this.message); }

// 【重要】将来的なAPI拡張に備えるための「未知のケース」を定義
class UnknownState extends RemoteState {
final String rawValue;
UnknownState(this.rawValue);
}

String mapStateToLabel(RemoteState state) {
// ここで網羅性を維持しつつ、未知のケースをハンドリングする
return switch (state) {
Loading() => ‘読み込み中…’,
Success(data: var d) => ‘成功: $d’,
Error(message: var m) => ‘エラー: $m’,

// この行があることで、将来 RemoteState にサブクラスが増えても、
// switch式で即座にエラーにならず、UnknownStateへフォールバックされる。
// かつ、コンパイラは網羅性を維持し続ける。
UnknownState() => ‘不明な状態’,
};
}

なぜこの設計が「美しい」のか

1. コンパイラの監視を止めない: `UnknownState` を明示的に追加することで、`switch` 式の網羅性は100%維持されます。
2. 型安全性の担保: `UnknownState` という明確な型があるため、デバッグ時に「未知のデータが混入した」という事実を型としてトレースできます。
3. 拡張に対する耐性: 新しい状態が追加されたとき、`switch` 式がエラーになるのではなく、明示的に `UnknownState` へのマッピングを検討する余地が生まれます。

—

パフォーマンスとVM内部の挙動への洞察

Dart VM(特にAOTコンパイル時)において、`switch` 式は高度に最適化されます。`sealed` クラスや列挙型を用いたパターンマッチングは、単なる `if-else` の連鎖ではなく、型タグや定数を用いたジャンプテーブルに近い効率的な機械語へと変換されます。

もしここで `default` や過剰な動的型付け(`dynamic`)に逃げると、VMは最適化のヒント(型推論の確実性)を失い、実行時の型チェックコストが増大します。

「網羅性を回避するなら、回避用の型を定義する」。これは単なるコードの書き方の問題ではなく、Dart VMの最適化パスを最大限活かすための戦略なのです。

—

チーフアーキテクトからの提言:避けるべき「アンチパターン」

最後に、コードレビューで私が即座にリジェクトする書き方を共有します。

  • `default: throw UnimplementedError();` を連発する:

これは「私は何も考えていません」という宣言です。将来的な仕様変更でクラッシュする爆弾をコードベースに埋め込んでいるのと同義です。

  • JSONパース時に無理やり `switch` を使う:

API連携のフロントエンドでは、JSONはあくまで「生のデータ」です。ビジネスロジックに渡す前に、必ず `UnknownState` を含む「ドメインモデル」へ変換(マッピング)してください。UI層で直接JSONのキーを `switch` して網羅性を回避するのは、アーキテクチャの汚染です。

まとめ

Dart 3の網羅性チェックは、単なる機能ではありません。それは「将来の自分やチームメンバーへの契約」です。

網羅性を回避したくなったときは、それは「設計の抽象度が足りない」というコンパイラからのサインです。`UnknownState` パターンを採用し、型システムを味方につけて、拡張性と堅牢性が共存する美しいコードを書いてください。

コードは書いた瞬間から「レガシー」への道を歩み始めます。その歩みを遅らせるのが、我々エンジニアの矜持です。

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