こんにちは!FlutterやDartの奥深い世界へようこそ。伝説的アーキテクトの私です。
今回は、Dart 3で導入された「switch式」による網羅性チェック(Exhaustiveness Checking)について、徹底的に解説していきますね。
他の言語からやってきた開発者や、プログラミングを学び始めたばかりの頃は、「switch文って、if/elseのすっきり版でしょ?」と思いがちです。しかし、Dart 3のswitch式が持つ本質は、単なる糖衣構文ではありません。「将来の自分やチームのうっかりミスを、コンパイラが完全に防いでくれる最強の安全装置」なのです。
ここをクリアすれば、あなたの書くコードの堅牢性は一気に跳ね上がりますよ。さあ、一緒に本質をマスターしていきましょう!
—
1. そもそも「switch文」と「switch式」は何が違うの?
従来の `switch` は「文(Statement)」でした。あくまで処理の流れを分岐させるだけで、値を直接返すことはできませんでしたよね。
一方、Dart 3で導入された `switch` は「式(Expression)」としても書けるようになりました。式ということは、変数に代入したり、関数の戻り値として直接スローしたりできるということです。
従来のswitch文(文)
// 従来の書き方:文なので代入が少し冗長になる
String getLabel(Status status) {
String label;
switch (status) {
case Status.active:
label = ‘有効’;
break;
case Status.inactive:
label = ‘無効’;
break;
}
return label;
}
進化したswitch式(式)
// Dart 3のswitch式:直接値を返すのでスマート!
String getLabel(Status status) => switch (status) {
Status.active => ‘有効’,
Status.inactive => ‘無効’,
};
どうですか?コードの美しさが段違いですよね。でも、本題はここからなんです。この書き方には、コンパイラによる「網羅性チェック」という強力な魔法が隠されています。
—
2. 網羅性チェック:コンパイラが「忘れているよ!」と怒ってくれる仕組み
例えば、アプリの仕様変更で、新しいステータスが追加されたとしましょう。
enum Status { active, inactive, pending } // ‘pending’ を追加!
もしあなたが従来の `switch` 文を使っていたらどうなるでしょうか? `pending` のケースを書き忘れても、多くの場合、コンパイルは普通に通ってしまいます。そして、アプリを実行して `pending` が渡された瞬間に、何も処理されずにバグを生むか、予期せぬ挙動を引き起こすことになります。
怖いですよね。でも、Dart 3のswitch式を使っていれば、そんな心配は一切いりません。
コンパイルエラーの発生イメージ
`pending` を処理し忘れた状態でコンパイルしようとすると、Dartのコンパイラ(および分析器)は次のように優しく、しかし厳しく教えてくれます。
> 「おいおい、`Status.pending` のケースが網羅されていませんよ!」
> (The type ‘Status’ is not exhaustively matched by the switch cases since it doesn’t match ‘Status.pending’)
これが網羅性チェック(Exhaustiveness Checking)です。
Dart VMやコンパイラの内部(AOTコンパイルのパイプライン)では、enumやsealedクラスの取りうるすべての状態(値域)を厳密に追跡しています。そのため、「すべてのパターンが網羅されているか」を静的に証明できない場合、コンパイルをビタッと止めてくれるのです。
—
3. 実践!安全なコードを設計してみよう
では、実際に動かせるコードで確認してみましょう。ここでは、ECサイトの注文状態を例にとります。
enum OrderState {
created, // 注文作成
paid, // 支払い完了
shipped, // 発送済み
cancelled, // キャンセル
}
String getOrderMessage(OrderState state) {
// switch式を使って、すべての状態をハンドリングする
return switch (state) {
OrderState.created => ‘ご注文ありがとうございます。お支払いをお待ちしております。’,
OrderState.paid => ‘お支払いが確認できました。発送準備を進めております。’,
OrderState.shipped => ‘商品が発送されました。到着までしばらくお待ちください。’,
OrderState.cancelled => ‘この注文はキャンセルされました。’,
};
}
void main() {
var currentState = OrderState.paid;
print(getOrderMessage(currentState));
// 出力: お支払いが確認できました。発送準備を進めております。
}
このコードは完璧に網羅されているため、何のエラーもなくビルド・実行できます。
もしケースを書き忘れたら……?
ここで、将来の機能追加で `OrderState` に `returned(返品)` が追加されたとします。しかし、`getOrderMessage` 関数をアップデートし忘れたとしましょう。
enum OrderState {
created,
paid,
shipped,
cancelled,
returned, // ← 追加された!
}
この瞬間、IDE(VS CodeやAndroid Studio)の画面上およびコンパイル時に、先ほどの網羅性エラーが即座に光ります。「あ、新しい状態が増えたから、対応するメッセージも追加しなきゃいけないんだな」と、コードを実行する前に100%気づくことができるのです。
これこそが、大規模開発やチーム開発においてバグを未然に防ぐ、最高に保守性の高い設計手法です。
—
4. 陥りやすい罠:`default` を安易に使わないこと
ここで一つ、実務でやりがちな「アンチパターン」についてお伝えしておきます。
「コンパイルエラーがうるさいから」という理由で、つい次のようなコードを書いてしまう人がいます。
// ⚠️ やってはいけないアンチパターン
String getOrderMessageBad(OrderState state) => switch (state) {
OrderState.created => ‘作成済み’,
OrderState.paid => ‘支払い済み’,
// ほかを全部ここに丸投げしちゃえ!
_ => ‘その他の状態または不明’,
};
ワイルドカードパターン(`_`)や `default` を使うと、確かにコンパイルエラーは消えます。しかし、これでは網羅性チェックの恩恵をドブに捨てているようなものです。
新しい `enum` の要素が追加されたとき、本来であればコンパイラが「ここを直して!」と教えてくれるはずが、`_` がすべてを吸い込んでしまうため、「意図しないフォールバック(予期せぬその他扱い)」になってしまいます。
原則
- 通常のenumやsealedクラスを扱うswitch式では、`_`(デフォルトケース)を使わないようにしましょう。
- すべてのケースを明示的に列挙することが、将来のバグを防ぐ最大の防壁になります。
—
5. まとめ
いかがでしたか? 今回はDart 3のswitch式と網羅性チェックについて解説しました。
- switch式は、値を直接返せるモダンで美しい構文である。
- 網羅性チェックにより、enumやsealedクラスの要素の消し忘れ・追加し忘れをコンパイル時に100%検知できる。
- 安易に `_`(デフォルト)に逃げず、すべてのケースを明示的に書くことで、保守性の高い堅牢なコードが保てる。
Dartという言語は、私たちの「うっかりミス」をコンパイラの力で徹底的にカバーしてくれる、非常に優しく、そして知的な言語です。この仕組みを味方につければ、大規模なFlutterアプリの改修も怖くありませんよ。
ここをクリアしたあなたなら、もうDartの制御構文はバッチリマスターできています!
明日からのコーディングで、ぜひ「網羅された美しいswitch式」を使ってみてくださいね。それでは、また次回の深淵なるDartの世界でお会いしましょう!