序文:なぜ今、あなたの「switch」は古いのか
Dart 3のリリースは、単なるマイナーアップデートではない。それはDartが「命令型言語の皮を被った、真に堅牢な静的型付け言語」へと進化を遂げたターニングポイントだ。
多くのエンジニアが、依然として `switch` を「値によって処理を分岐させるためのステートメント(文)」として扱っている。しかし、現代的なDart開発、特に大規模なFlutterアプリケーションのアーキテクチャ設計において、その認識はもはや負債でしかない。
今、我々が手にしたのは 「switch式(Switch Expression)」 だ。これは単なる記述の簡略化ではない。コンパイラに「網羅性(Exhaustiveness)」を保証させ、実行時の例外をコンパイル時に握りつぶすための、極めて強力な武器である。
本稿では、このswitch式がいかにしてボイラープレートを削ぎ落とし、型安全性を極限まで高めるのか。その真髄を解説する。
—
1. 「文」から「式」へのパラダイムシフト
従来の `switch` 文は、副作用を伴う「手続き」を記述するためのものだった。変数を宣言し、その値を `switch` 内で書き換える。このアプローチには、常に「初期化忘れ」や「意図しないフォールスルー」の危険がつきまとう。
[Before] 冗長で脆い命令型コード
// 命令的な記述:変数がミュータブル(可変)であり、代入漏れのリスクがある
String getStatusMessage(Status status) {
String message; // 初期化されていない
switch (status) {
case Status.loading:
message = ‘読み込み中…’;
break;
case Status.success:
message = ‘成功しました’;
break;
case Status.error:
message = ‘エラーが発生しました’;
break;
// defaultを忘れると、未初期化のmessageが返るリスクがある
}
return message;
}
[After] 宣言的で堅牢なswitch式
Dart 3のswitch式を使えば、代入という「手続き」を排除し、値を直接「定義」できる。
// 宣言的な記述:値が直接返される。不変(final)を維持できる。
String getStatusMessage(Status status) => switch (status) {
Status.loading => ‘読み込み中…’,
Status.success => ‘成功しました’,
Status.error => ‘エラーが発生しました’,
};
ここで重要なのは、`=>`(アロー)の右側が評価された瞬間に値が確定する点だ。`break` は不要であり、何より「値を返さないルート」が存在する場合、Dartコンパイラが即座にエラーを吐く。これが「網羅性チェック」の威力だ。
—
2. Sealedクラスとの融合:コンパイラを最強のデバッグパートナーにする
switch式の真価が発揮されるのは、`sealed` クラス(封印されたクラス)と組み合わせた時だ。
`sealed` クラスを使用すると、そのクラスのサブタイプが同一ライブラリ内に限定されることがコンパイラによって保証される。これにより、「全てのケースを網羅しているか」を静的に解析できるようになる。
プロダクション・グレードの実装例:APIレスポンスのハンドリング
Webエンジニアが最も頻繁に遭遇する「非同期通信の状態管理」を例に、堅牢な設計を見てみよう。
// 状態を sealed クラスで定義。これにより、これ以外の状態は存在しないことが保証される。
sealed class FetchState
class Loading
class Success
final T data;
Success(this.data);
}
class Failure
final Exception exception;
Failure(this.exception);
}
// UIコンポーネント内での利用例
String buildUI
return switch (state) {
// 型プロモーションが自動で効くため、state.data に直接アクセス可能
Loading() => ‘Skeleton Loaderを表示…’,
Success(data: var content) => ‘データ表示: $content’,
Failure(exception: var e) => ‘エラーログ: ${e.toString()}’,
// ここで Failure を書き忘れると、コンパイルエラーになる。
// 「default:」や「_ =>」を書く必要はない。むしろ書かない方が安全だ。
};
}
なぜ `default` を書いてはいけないのか?
多くの開発者は、癖で `_ => ‘Unknown’` のようなデフォルトケースを書きがちだ。しかし、`sealed` クラスを使っている場合、それはアンチパターンとなる。
もし将来、`FetchState` に `Maintenance` という新しい状態が追加されたとしよう。デフォルトケースがあると、コンパイラは警告を出さず、実行時に意図しない挙動(’Unknown’ の表示)を引き起こす。デフォルトケースを排除していれば、コンパイラが「`Maintenance` が網羅されていません」と叱ってくれる。「コンパイルが通る = 全ての状態を考慮済み」という状態を作るのがプロの設計だ。
—
3. ガード句(when)による高度な条件分岐
単純なパターンマッチングだけでは解決できない、複雑なビジネスロジックもswitch式ならエレガントに記述できる。`when` キーワードによるガード句の導入だ。
String describeTransaction(double amount, {required bool isAdmin}) {
return switch (amount) {
// ガード句を用いた条件付マッチング
> 1000000 when !isAdmin => ‘高額決済:管理者の承認が必要です’,
> 1000000 => ‘高額決済:処理を継続します’,
<= 0 => ‘無効な金額です’,
_ => ‘決済受付完了’,
};
}
Dart VMはこの `when` 句を効率的に評価する。複数の `if-else` を連ねるよりも、論理的な構造が明確になり、コグニティブ・ロード(認知負荷)を劇的に下げることができる。
—
4. パフォーマンスと内部動作の視点
「式が増えると実行速度が落ちるのではないか?」という懸念を持つかもしれない。しかし、その心配は無用だ。
1. Jump Tableの最適化: Dart VMおよびAOTコンパイラは、switch式(特にEnumやSealedクラス)を評価する際、可能な限り効率的なジャンプテーブルや決定木へとコンパイルする。
2. 型プロモーションの静的確定: パターンマッチングによる型プロモーションは、コンパイル時に解決される。実行時に `is` 演算子を連打するよりも、型安全かつ高速だ。
3. メモリ効率: 無駄なローカル変数の宣言(`var x;`)を減らし、`final` な定数として値を扱えるため、コンパイラによる最適化の余地が広がる。
—
5. 結論:コードレビューで何を指摘すべきか
あなたがリードエンジニアとしてチームのコードを見る際、以下の基準で switch 式への移行を促してほしい。
- 「その変数は `final` にできるか?」: switch文の中で代入を繰り返しているなら、switch式に書き換えて `final` な定数定義に変えさせるべきだ。
- 「`default` ケースは本当に必要か?」: `enum` や `sealed class` を対象にしているなら、`default` を削除させ、網羅性チェックの恩恵をフルに受けさせるべきだ。
- 「ロジックが散らばっていないか?」: 複数の `if` 文で型のチェックと中身の取り出し(キャスト)を行っているなら、パターンのデストラクト(構造分解)を使って一行にまとめさせるべきだ。
Dart 3のswitch式は、単なる見た目の変更ではない。それは、「バグが入り込む隙間を、言語仕様レベルで物理的に塞ぐ」ためのツールである。
この「式の力」を掌握し、あなたのプロダクトを、より堅牢で、より美しいものへと昇華させてほしい。