【入門編】Dartの「switch式」で網羅性チェックを強制し、将来のバグを防ぐ設計指針 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartを使った開発を楽しんでいますか?

他のプログラミング言語(例えばJavaやTypeScriptなど)からDartの世界にやってくると、制御構文の進化のスピードに驚かされることがありますよね。特に、Dart 3で導入された「switch式(Switch Expressions)」と「網羅性チェック(Exhaustiveness Checking)」の組み合わせは、コードの安全性と保守性を劇的に引き上げてくれる最高の発明の一つです。

今回は、この強力な仕組みを使って「将来のバグをコンパイルエラーで未然に防ぐ設計指針」について、本質から分かりやすく紐解いていきますね。ここをクリアすれば、あなたの書くDartコードの堅牢性はプロのアーキテクト水準に到達しますよ!

—

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

これまでの古いDart(あるいは多くの伝統的な言語)では、`switch`文は単なる「分岐の命令(Statement)」でした。そのため、条件漏れがあってもコンパイラは何も言ってくれず、実行時になって「あれ、予期しない値が来たぞ?」とバグ(あるいは予期せぬフォールススルー)を生む原因になっていました。

しかし、Dart 3のswitch式は、値を返します(関数のように右辺に書ける)。そして何より素晴らしいのが、「取りうるすべてのパターンを網羅しているか」をDartのコンパイラが厳格にチェックしてくれる(網羅性チェック)点です。

もし将来、状態が増えたのに分岐の処理を書き忘れたら?
——実行するまでもなく、コンパイルエラーとして赤く波線が教えてくれるようになります。これが最高に心地よく、かつ安全なんです。

—

2. 実践!sealed classで網羅性チェックを強制する設計

百聞は一見に如かず。実際のコードを見てみましょう。
ここでは、アプリの「通信状態(UI State)」を表現するモデルを考えてみます。

// 【重要】sealed修飾子を付けることで、このファイルの外側で
// 勝手に新しい子クラスを作れなくし、パターンの閉じられた世界を作ります。
sealed class UiState {}

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

ここに、switch式を使って画面に表示するメッセージを返す関数を作ってみます。

String getMessage(UiState state) {
// switchが「式」になっていることに注目! returnを何度も書く必要がありません。
return switch (state) {
Loading() => ‘データを読み込んでいます…’,
Success(data: val) => ‘成功しました: $val’,
Error(message: err) => ‘エラーが発生しました: $err’,
};
}

このコードは完璧にコンパイルを通過します。`Loading`、`Success`、`Error`のすべてのケース(パターン)が漏れなく網羅されているからです。

—

3. 将来のバグを防ぐ瞬間(仕様変更のシミュレーション)

さて、ここからが本題です。
アプリの仕様変更で、「メンテナンス中(Maintenance)」という新しい状態を追加することになったとしましょう。

sealed class UiState {}

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

// 【新しく追加された状態】
class Maintenance extends UiState {}

この状態で、先ほどの `getMessage` 関数を修正し忘れたままにするとどうなるでしょうか?

Dartのコンパイラは、慈悲なく次のようなコンパイルエラーを突きつけてきます。

> エラー: The type ‘UiState’ is not exhaustively matched by the switch cases since it doesn’t match ‘Maintenance’.
> (訳:UiState型は Maintenanceケースを網羅していないため、switchの網羅性チェックを満たしていません)

String getMessage(UiState state) {
return switch (state) {
Loading() => ‘データを読み込んでいます…’,
Success(data: val) => ‘成功しました: $val’,
Error(message: err) => ‘エラーが発生しました: $err’,
// ほら、Maintenance() の書き忘れをコンパイラがここで検知してくれます!
};
}

開発者は「あ、新しい状態を追加したから、ここも処理を書き足さないといけないんだな」と即座に気づくことができます。テスト環境や、ひどい場合は本番リリース後のユーザーの画面で突然アプリがクラッシュするような悲劇を、コンパイルエラーの時点で100%防ぐことができるのです。これが、設計における網羅性チェックの真価です。

—

4. 陥りやすい罠:`default` や `_`(ワイルドカード)の安易な使用に注意

ここで一つ、実務でやりがちな「落とし穴」についてお話しておきます。

「コンパイルエラーがめんどくさいから」といって、switch式の最後に `_ => ‘その他’` のようなワイルドカード(デフォルト動作)を安易に置いてしまうことがあります。

String getMessage(UiState state) {
return switch (state) {
Loading() => ‘読み込み中’,
Success(data: val) => val,
_ => ‘その他’, // ← これを書いてしまうと…
};
}

これをしてしまうと、新しい状態(Maintenanceなど)を追加したときにコンパイルエラーが発生しなくなってしまいます。 なぜなら、`_` が新しい状態をも「その他」として吸い込んでしまうからです。

黄金の設計指針

  • ビジネスロジックの中核や、UIの状態分岐など「すべてのケースを明示的にハンドリングすべき場所」では、`_` や `default` を使わない。
  • あえて網羅性チェックを効かせ、新しい状態の追加漏れをコンパイラに検知させる。

このルールを守るだけで、コードベースのメンテナンス性は劇的に向上します。

—

まとめ

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

  • Dart 3のswitch式は値を返す洗練された構文であること。
  • `sealed class` と組み合わせることで、網羅性チェックが強制されること。
  • 将来の仕様変更による「修正漏れ(ヒューマンエラー)」をコンパイラが完全に防いでくれること。

これらを味方につけると、大規模なFlutterアプリを構築する際も、恐れずにガンガンリファクタリングや機能追加ができるようになります。

「ここをこう変えたらコンパイラはどう反応するかな?」という視点を持てると、Dartという言語の奥深さがより一層楽しくなってきますよ。
今日の知識を、ぜひ明日のコーディングから活かしてみてくださいね。それでは、また!

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