こんにちは!FlutterやDartを使った開発を楽しんでいますか?
他のプログラミング言語からDartにやってくると、Dart 3で導入されたパターンマッチングとswitch式(Switch Expressions)の強力さに驚くことが多いですよね。特に、コンパイラが「おっと、そのケースを忘れてますよ!」と教えてくれる網羅性チェック(Exhaustiveness Checking)は、バグを未然に防ぐ最強の武器です。
今回は、この網羅性チェックがコンパイラの裏側でどう働いているのか、そして「どうしても特定のケースを無視したい」という場合の安全な回避策について、優しく、そして深く紐解いていきましょう。
ここをクリアすれば、Dartの制御構文の理解はバッチリマスターできますよ!
—
1. switch「文」からswitch「式」へ:Dart 3のパラダイムシフト
これまでのDartでは、`switch`はあくまで「文(Statement)」でした。値を返すことができず、`break`を書き忘れると次のケースに処理が流れ落ちる(フォールスルー)という、バグの温床になりやすい仕様でしたよね。
しかし、Dart 3のswitch式は違います。`if-else`の三項演算子のように、評価された結果の「値」を直接返すことができます。
// switch式の基本形
String getTrafficLightMessage(String color) => switch (color) {
‘red’ => ‘止まれ’,
‘yellow’ => ‘注意’,
‘green’ => ‘進む’,
_ => ‘不明な色’,
};
このように、コードが非常にシンプルになり、副作用のない純粋な値の変換ができるようになります。
—
2. コンパイラはどうやって見ている?網羅性チェックのメカニズム
さて、ここからが本題です。Dartのコンパイラ(CFA: Control Flow Analyzer)は、switch式が評価されるとき、「入力され得るすべての可能性(値のドメイン)が、網羅的に処理されているか」を静的に解析しています。
例えば、以下のような`enum`(列挙型)を考えてみましょう。
enum StatusCode { success, unauthorized, notFound, serverError }
String handleResponse(StatusCode code) {
return switch (code) {
StatusCode.success => ‘成功です’,
StatusCode.unauthorized => ‘認証エラーです’,
StatusCode.notFound => ‘見つかりません’,
// StatusCode.serverError が抜けている!
};
}
このコードをコンパイルしようとすると、Dartのコンパイラは次のようなエラーを吐いて怒ってきます。
> The type ‘StatusCode’ is not exhaustively matched by the switch cases.
> (型 ‘StatusCode’ のすべてのケースが網羅されていません。)
なぜコンパイラはこれがわかるのか?
Dartの型システムにおいて、`enum`やシールドクラス(Sealed Class)は、「取りうる値の集合が完全に閉じている(有限である)」と定義されます。
コンパイラは、その集合(型)の全要素と、switchの各アーム(case)でマッチングされているパターンを集合論的に比較し、「差集合(まだカバーされていない値)」が空(空集合)であるかを数学的に検証しているのです。
この仕組みがあるおかげで、将来チームのメンバーが新しいステータス(例: `StatusCode.timeout`)を`enum`に追加した瞬間、その変更を反映すべきすべてのswitch式でコンパイルエラーが発生し、修正漏れを100%防ぐことができるというわけですね。
—
3. 「すべてのケースを書きたくない!」時の安全な回避策
「いやいや、理論上は全ケースあるけど、実質的にこの2つ以外は絶対にありえないんだよ!」という場面や、「残りのケースは全部まとめて『その他』として処理したい」というケースもありますよね。
そんな時、どうすれば安全にコンパイラの網羅性チェックをクリアできるでしょうか? 現場で使える2つのアプローチを見ていきましょう。
① ワイルドカードパターン(`_`)を使う
もっとも一般的で安全な方法は、デフォルトケースとしてワイルドカード `_` を使うことです。
String handleResponseSafe(StatusCode code) {
return switch (code) {
StatusCode.success => ‘成功です’,
// 残りのケースをすべてキャッチする
_ => ‘その他の予期せぬエラー’,
};
}
【知見のポイント】
ワイルドカード `_` を置いた瞬間、ドメインの残りの無限(あるいは有限の残り)の空間がすべてカバーされるため、コンパイラの網羅性チェックは「合格」になります。
② あえて例外を投げる(将来の変更への備え)
「ワイルドカードを使うと、将来新しい`enum`が追加された時にコンパイラが教えてくれなくなるのでは?」と勘の鋭いあなたは思ったかもしれません。その通りです! `_` ですべてを丸めてしまうと、新しい値が追加された時にサイレントに「その他」扱いにされてしまい、気づきにくくなります。
そこで、「型安全性を維持しつつ、あり得ないケースには例外を投げる」というテクニックが使われます。
String handleStrictResponse(StatusCode code) {
return switch (code) {
StatusCode.success => ‘成功です’,
StatusCode.unauthorized => ‘認証エラーです’,
StatusCode.notFound => ‘見つかりません’,
StatusCode.serverError => ‘サーバーエラーです’,
// 万が一、将来 enum が追加されてここに到達したら即座にクラッシュさせる
// 実際には型が網羅されているため、このコードに到達することはない
};
}
もしすべてのケース(この例では4つすべて)を書き切っていれば、ワイルドカード `_` を書かなくても網羅性チェックがパスするため、コンパイルエラーになりません。
そして、将来新しい`enum`が追加された瞬間にコンパイルエラーが復活するため、「網羅性の安全性を保ちつつ、冗長なデフォルト分岐を書かずに済む」という、プロとして最も推奨される美しい書き方になります。
—
まとめ
今回はDart 3のswitch式における網羅性チェックの裏側と、その実用的なコントロール方法について解説しました。
- 網羅性チェックは、Dartの型システムとコンパイラが、値の集合の漏れを数学的に検知する強力な機能。
- `enum`などの全ケースを書き切ることで、ワイルドカードを使わなくてもコンパイルを通すことができる。
- 将来の仕様変更に備えて、安易に `_` で逃げず、型による完全な網羅を目指すのがDartらしいクリーンなコードの書き方。
このコンパイラとの対話を楽しみながらコードを書けるようになると、あなたのDartコードはより堅牢で、バグ知らずのものに生まれ変わりますよ。
それでは、次回の記事もお楽しみに!バッチリDartをマスターしていきましょう!