【入門編】Dartのswitch式における「網羅性チェック(Exhaustiveness Checking)」のメカニズム – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの開発現場で、日夜コードと向き合っている先輩エンジニアです。

今日は、Dart 3で導入された「switch式」と、その真骨頂である「網羅性チェック(Exhaustiveness Checking)」についてお話ししますね。

「switch文」なら昔から他の言語でも使っていたよ、という方も多いと思います。しかし、Dart 3で劇的に進化されたswitch式と網羅性チェックのコンパイル時メカニズムを理解すると、あなたの書くコードの安全性と美しさは次元が変わります。

ここをクリアすれば、Dartの型システムの本質をバッチリマスターできますよ。一緒に深掘りしていきましょう!

—

1. そもそも「網羅性チェック」ってなに?

日常のたとえ話をさせてください。
例えば、あなたがカフェで「コーヒー」「紅茶」「ジュース」の3種類のドリンクチケットを発行するとします。受付係のロボットに「もしチケットが来たらこう動いてね」と指示を出すとき、もし「コーヒー」と「紅茶」の処理しか教えていなかったらどうなるでしょう?

「ジュース」のチケットが持ち込まれた瞬間、ロボットはフリーズするか、予期せぬエラーを起こしてしまいますよね。

プログラムの世界もこれと全く同じです。
特に状態のパターンが多いUIの構築や、APIレスポンスのハンドリングにおいて、「ありえるケースのどれかひとつでも処理のし忘れ(抜け)があったら、コンパイラがビルドを絶対に許さない」という強力な仕組み、それが網羅性チェックです。

Dart 3のコンパイラ(CFA: Control Flow Analysis / 制御フロー解析エンジン)は、渡されたデータの型が取りうるすべての状態(バリエーション)を静的に追跡し、「おい、このケースの処理が抜けているぞ!」とコンパイルエラーで教えてくれるのです。

—

2. 実践! `sealed class` と `enum` で網羅性を体感する

百聞は一見にしかず。実際のコードを見てみましょう。
ここでは、アプリの読み込み状態(State)を表現する最も美しいパターンとして、`sealed class`(シールクラス)を使ってみます。

// 1. 状態を表現する sealed class を定義
sealed class AppState {}

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

ここで `sealed` という修飾子を使っているのがポイントです。
`sealed class` を使うと、「このファイルをインポートしているスコープの外からは、新しい子クラスを追加することはできない」という制約がコンパイラによって強制されます。つまり、コンパイラは「このアプリの状態は、`Initial`, `Loading`, `Success`, `Error` の4つが世界のすべてだ」と完全に把握できるのです。

では、この状態を受け取って画面に表示するメッセージを返す関数を、Dart 3の switch式 で書いてみましょう。

String handleState(AppState state) {
// switchは「文」ではなく「式」なので、そのままreturnできる
return switch (state) {
Initial() => ‘アプリを起動しています…’,
Loading() => ‘データを読み込み中…’,
Success(data: var d) => ‘成功しました! データ: $d’,
Error(message: var m) => ‘エラーが発生しました: $m’,
// すべてのケースを網羅しているので、これでコンパイルが通ります!
};
}

もしケースを書き忘れたらどうなる?

ここで、開発の途中でうっかり `Error` のケースを書き忘れたと想像してください。

String handleState(AppState state) {
return switch (state) {
Initial() => ‘アプリを起動しています…’,
Loading() => ‘データを読み込み中…’,
Success(data: var d) => ‘成功しました! データ: $d’,
// Errorケースの処理が抜けている!
};
}

このコードをDartのコンパイラ(`dart compile` や Flutterのビルドプロセス)に食わせると、実行するまでもなく、以下の恐ろしく知的で親切なエラーが突きつけられます。

> コンパイルエラー:
> The type ‘AppState’ is not exhaustively matched by the switch cases since it doesn’t match ‘Error’.
> (型 ‘AppState’ は ‘Error’ を網羅していないため、網羅的にマッチしていません。)

最高だと思いませんか?
「後から新しい状態クラスを追加したはいいものの、画面側の分岐処理でハンドリングし忘れて、本番環境で突然アプリがクラッシュする」という、世の中の開発者を長年苦しめてきた悪夢から、Dartのコンパイラが完全に守ってくれるのです。

—

3. `enum` でも強力に機能する

もちろん、従来の `enum` でも Dart 3 の switch式と網羅性チェックは完璧に機能します。

enum Weather { sunny, rainy, cloudy, snowy }

String getWeatherAdvice(Weather weather) {
return switch (weather) {
Weather.sunny => ‘サングラスを持っていきましょう!’,
Weather.rainy => ‘お気に入りの傘を忘れずに。’,
Weather.cloudy => ‘過ごしやすい一日になりそうですね。’,
Weather.snowy => ‘足もとに気をつけて温かくして出かけましょう。’,
};
}

もしここで `Weather.snowy` の分岐を消し忘れたら、先ほどと同様にコンパイラが「`Weather.snowy` が処理されていません!」と教えてくれます。

—

4. 陥りやすい罠と「お助けアイテム」 `default` / `_`

ここまで聞くと「じゃあ、万が一のために常に `_`(ワイルドカード:デフォルトケース)を書いておけば、エラーにならなくて楽なんじゃないか?」と思われるかもしれません。

// ⚠️ 避けるべきアンチパターン
String handleStateBad(AppState state) {
return switch (state) {
Initial() => ‘初期状態’,
_ => ‘その他すべて’, // これを書くと網羅性チェックが機能しなくなる!
};
}

ここで重要な注意点があります。
`_` や `default` を書いてしまうと、「将来、新しい状態クラス(例: `Maintenance` など)を追加したときに、コンパイラがエラーで教えてくれなくなる」というデメリットが発生します。 `_` がすべての未定義ケースを吸い取ってしまうためですね。

そのため、「sealed classやenumの網羅性チェックの恩恵を最大限に受けるためには、基本的には `_` を使わず、ありうるケースをすべて明示的に並べる」のが、モダンなDartにおけるベストプラクティスです。

どうしても将来の拡張に備えてフォールバックを入れたい場合を除き、コンパイラの厳格なチェックをあえて受けることが、堅牢なコードへの近道ですよ。

—

まとめ

いかがでしたでしょうか?

  • Dart 3のswitch式は、値を返す洗練された構文である。
  • `sealed class` や `enum` と組み合わせることで、コンパイラが型の世界を完全に追跡する。
  • すべてのケースが網羅されていないと、実行する前にコンパイルエラーで気づかせてくれる。

このメカニズムを味方につけると、コードを書いている段階でバグの芽をバシバシ摘み取ることができます。もう「書き忘れによるクラッシュ」に怯える必要はありません。

ぜひ明日のFlutter/Dart開発から、この強力な網羅性チェックをガンガン活用してみてくださいね。あなたのコードが、より美しく、より堅牢になることを応援しています!

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