こんにちは!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開発を楽しんでいきましょう!