【入門編】Dart 3のswitch式における「網羅性チェック」を回避する際の落とし穴と安全な設計 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!Dartのコアな仕組みや近代的な言語機能の世界へようこそ。
今回は、Dart 3で導入されてからすっかり開発の主役になった「パターンマッチング」と、その強力な武器である「網羅性チェック(Exhaustiveness Checking)」、そして実務で必ず直面する「その落とし穴と安全な回避策」について深く掘り下げていきますね。

「switch式を書いたのに、なぜかコンパイルエラーが消えない…」「外部ライブラリの型を処理したいだけなのに厄介だな…」そんなモヤモヤを抱えたことはありませんか?

ここをしっかりとクリアすれば、あなたのDartコードは一気に堅牢でモダンなものになりますよ。さあ、一緒に本質をマスターしていきましょう!

—

1. Dart 3の真骨頂:網羅性チェックとは?

まずは基本のおさらいから。Dart 3では、従来の「文(Statement)」としての `switch` に加え、値を返す「式(Expression)」としての `switch` が使えるようになりました。

これに伴い、コンパイラ(Dart VM / AOTコンパイラ)は、「すべての取りうるパターンが網羅されているか」をコンパイル時に厳密に検証するようになりました。これが網羅性チェックです。

例えば、以下のような `sealed class`(封印されたクラス階層)を考えてみましょう。

sealed class NetworkResult {}
class Success extends NetworkResult {
final String data;
Success(this.data);
}
class Error extends NetworkResult {
final Exception exception;
Error(this.exception);
}

この `NetworkResult` を `switch` 式で処理する場合、Dartのコンパイラは「`Success` と `Error` の両方がハンドリングされているか」をチェックします。

String handleResult(NetworkResult result) {
return switch (result) {
Success(data: var d) => ‘成功: $d’,
// Errorケースが抜けていると、ここでコンパイルエラー!
};
}

もし `Error` ケースを書き忘れると、コンパイラが「おいおい、`Error` の場合の処理が抜けてるよ!」と教えてくれます。これがバグを未然に防ぐ、最高に強力な仕組みですよね。

—

2. 網羅性チェックが「機能しなくなる」落とし穴

さて、ここからが本題です。この完璧に見える網羅性チェックですが、「自分のコントロール外にある型」を扱うときに、思わぬ落とし穴が出現します。

それが、外部パッケージ(Pub.devのライブラリなど)で定義された `sealed class` や `enum` を拡張・変更された場合です。

罠:将来の拡張に対する恐怖

あなたが使っている外部ライブラリの作者が、次のようなアップデートを行ったとします。

// 外部ライブラリのコード(あなたは変更できない)
sealed class NetworkResult {}
class Success extends NetworkResult { … }
class Error extends NetworkResult { … }
class Loading extends NetworkResult { … } // ← 【新しく追加された!】

このライブラリをあなたのプロジェクトでアップデートした瞬間、あなたのコードベースにあるすべての `switch (result)` で網羅性チェックが働き、コンパイルエラー(爆発)が起きます。

「いやいや、新しく追加された `Loading` なんて今の画面では興味ないよ!とりあえず既存の挙動を維持したいのに…!」という場面でも、コンパイラは容赦なくエラーを出してきます。

—

3. 安全にデフォルトケースを処理するベストプラクティス

では、こうした外部要因による変更リスクから身を守りつつ、安全に網羅性をコントロールするにはどうすればよいのでしょうか?

ここで登場するのが、ワイルドカードパターン(`_`)を用いたフォールバック(デフォルトケース)の設計です。

パターンA: 予期せぬ未来に備える `_` (ワイルドカード)

すべてのケースを列挙した上で、将来追加されるかもしれない未知のケースを拾うために `_` を置く方法です。

String handleResultSafely(NetworkResult result) {
return switch (result) {
Success(data: var d) => ‘成功: $d’,
Error(exception: var e) => ‘エラー: $e’,

// 【ベストプラクティス】
// 将来、ライブラリ側で新しいサブクラスが追加されても、
// ここに落ちるためコンパイルエラーを防げます。
_ => ‘その他の予期せぬ状態’,
};
}

【知的なポイント】
ここで重要なのは、「あえて `_` を置くことで、コンパイラの厳格な網羅性チェックを意図的に無効化(フォールバック)している」という点です。
すべてをきっちり縛りたいドメインロジック(社内コード)では `_` は厳禁ですが、外部依存の境界線(APIレスポンスやライブラリの型)では、この `_` が防波堤として機能します。

パターンB: あえてエラーを投げる(Fail-Fastの思想)

「いや、新しい状態が追加されたら、アプリで無視するのではなく、デベロッパーが即座に気づいてコードを書き換えるべきだ!」という堅牢性を重視する設計もあります。

String handleResultStrict(NetworkResult result) {
return switch (result) {
Success(data: var d) => ‘成功: $d’,
Error(exception: var e) => ‘エラー: $e’,

// 未知の型が来たら例外を投げて即座に落とす(開発者への強いシグナル)
_ => throw StateError(‘未対応のNetworkResult型です: ${result.runtimeType}’),
};
}

このアプローチは、ライブラリのアップデートに伴う仕様変更を検知しやすくするため、大規模なチーム開発において非常に有効です。

—

4. まとめ:Dart VMとコンパイラの視点を持つ

Dartのコンパイラ(Front End / CFE)は、ソースコードを解析してAST(抽象構文木)を構築し、パターンマッチングの網羅性を数学的に証明しています。

  • 内部のドメインモデル:網羅性チェックを100%活かし、`_` を使わずにすべての型を列挙する(型安全の極み)。
  • 外部の境界(APIやライブラリ):`_` や `throw` を適切に配置し、将来の変更耐性を持たせる。

この使い分けができるようになると、ただ動くだけのコードから、保守性が高くプロフェッショナルなDartコードへとステップアップできますよ。

ここをクリアしたあなたなら、もうDart 3の制御構文で迷うことはありません。自信を持ってモダンなDart開発を楽しんでいきましょう!

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