こんにちは。プロダクトのコードレビューをしていると、未だにDart 2時代の古い発想を引きずった冗長な`switch`文や、ネストが深い`if-else`の嵐に遭遇することがあります。
フロントエンド開発やコンポーネントの状態管理、非同期API連携のパーシングレイヤーにおいて、「複数の異なる入力値に対して、同一のドメインロジック(UIコンポーネントの状態、エラーハンドリング、ルーティングなど)を適用したい」という要件は日常茶飯事です。
今回は、Dart 3で導入されたswitch式(Switch Expressions)と論理パターン(Logical-or Pattern: `|`)を組み合わせ、可読性を極限まで高めつつ、コンパイラ最適化の恩恵を最大限に受けるための実践的テクニックを伝授します。
—
なぜ従来の `switch` 文ではダメなのか?
まずは、コードレビューで私が即座にリジェクトする「悪臭(Bad Smell)」を放つコードを見てください。
// 【アンチパターン】可読性が低く、フォールスルーのバグを孕む旧来のswitch文
String getComponentState(int statusCode) {
String state;
switch (statusCode) {
case 200:
case 201:
case 202:
state = ‘SUCCESS’;
break;
case 400:
case 401:
case 403:
state = ‘CLIENT_ERROR’;
break;
case 500:
case 502:
case 503:
state = ‘SERVER_ERROR’;
break;
default:
state = ‘UNKNOWN’;
}
return state;
}
このコードの何が問題でしょうか?
1. 冗長なボイラープレート: 各`case`に値を代入し、`break`を書き忘れるとフォールスルー(意図しない処理の継続)によるクリティカルなバグを生む。
2. 不変性(Immutability)の破壊: 変数 `state` を一度宣言してミュータブル(可変)に再代入している。これは関数型プログラミングのパラダイムに反し、認知負荷を高めます。
3. 網羅性(Exhaustiveness)の欠如: 新しいHTTPステータスが増えた際、コンパイラが警告してくれません。
これをDart 3のswitch式と論理orパターンで書き換えると、劇的に洗練されます。
—
Dart 3 `|` (Logical-or) パターンによる極限のスマート化
Dart 3では、`switch`が「文(Statement)」から「式(Expression)」に進化しました。式であるため、変数代入の右辺に直接置くことができ、不変性を強制できます。
さらに、複数のパターンを `|`(パイプ)で結合することで、1つの分岐アーム(Arm)に複数の条件を集約可能です。
プロダクションコード例:APIレスポンスのドメインマッピング
WebフロントエンドやAPI連携クライアントで頻出する、ステータスコードに応じたUIコンポーネントの状態マッピングを例に取ります。
enum UiComponentState { success, warning, error, offline }
/// HTTPステータスコードおよびネットワーク例外から、
/// Dart 3のswitch式と論理orパターンを用いてUIの状態を堅牢に導出する
UiComponentState resolveComponentState(int statusCode) {
return switch (statusCode) {
// 200番台の成功系を論理orで結合
200 || 201 || 202 => UiComponentState.success,
// クライアント起因のエラー群
400 || 401 || 403 || 404 => UiComponentState.warning,
// サーバー起因の致命的エラー群
500 || 502 || 503 || 504 => UiComponentState.error,
// ネットワーク接続断やタイムアウトを模擬するカスタムコード
0 || 408 || 504 => UiComponentState.offline,
// デフォルト(網羅性を担保するためのガード)
_ => UiComponentState.error,
};
}
このコードが優れている理由
1. 圧倒的な宣言的記述: 「この値、またはこの値、またはその値」というビジネスロジックが、数学の集合論のように美しく表現されています。
2. ミュータビリティの排除: 変数の再代入がなくなり、関数の返り値として直接 `switch` 式の結果をスローするため、コンパイラがレジスタ割り当てやインライン化の最適化を行いやすくなります。
3. 網羅性チェック(Exhaustiveness Checking):
もし対象が `enum` やシールドクラス(Sealed Class)である場合、DartのCFA(Control Flow Analysis)がすべての可能性を網羅しているかをコンパイル時に検証します。網羅されていない場合、コンパイルエラーとして検知できるため、実行時エラーをゼロに近づけられます。
—
発展:ガード節(`when`)との組み合わせによる高度な条件分岐
実務では、単なる値の一致だけでなく、「特定の範囲内かつ特定のフラグが立っている場合」といった複雑な条件が絡みます。そんなときは、論理orパターンに `when` 句(ガード節)を組み合わせます。
sealed class NetworkResult {}
class Success extends NetworkResult { final int code; Success(this.code); }
class Failure extends NetworkResult { final int errorCode; Failure(this.errorCode); }
class Loading extends NetworkResult {}
String handleNetworkResult(NetworkResult result, bool isMaintenanceMode) {
return switch (result) {
// メンテナンス中の特定エラー
Failure(errorCode: 500 || 503) when isMaintenanceMode => ‘現在、メンテナンス中です。’,
// 通常のエラーコード群
Failure(errorCode: 400 || 401) => ‘認証またはリクエストに問題があります。’,
Failure(errorCode: 500 || 502 || 503) => ‘サーバー側で一時的な障害が発生しています。’,
// 成功系
Success(code: 200 || 201) => ‘処理が正常に完了しました。’,
// ローディング中やその他
Loading() => ‘読み込み中…’,
_ => ‘予期せぬエラーが発生しました。’,
};
}
ここで注目すべきは、`Failure(errorCode: 500 || 503)` のように、オブジェクトのプロパティ抽出(Object Pattern)と論理orパターンをネストさせている点です。これにより、ディープな階層構造を持つJSONのパース結果や、複雑な状態ツリーを持つウィジェットの状態管理であっても、浅く美しいフラットな分岐を実現できます。
—
パフォーマンスとコンパイラ最適化の知見
「こんなに複雑なパターンマッチングを書くと、実行時コスト(パフォーマンス)が高くなるのではないか?」と懸念するアーキテクトもいるかもしれません。
Dart VMおよびAOTコンパイラ(dart2native)の内部構造を知る者として断言しますが、心配無用です。
- ジャンプテーブル(Jump Tables)への最適化:
コンパイラは、多数の `||` で結合された定数リテラル値の比較を、内部的に効率的なジャンプテーブルやバイナリサーチツリーにコンパイルします。冗長な `if-else` チェーンよりも、VMのバイトコード実行時、あるいはネイティブマシン語への変換時において、分岐予測のヒット率を高める構造に最適化されます。
- メモリ効率:
一時変数を生成しない式(Expression)ベースの記述は、スタックフレームの消費を最小限に抑え、ガベージコレクタ(GC)への負荷を完全にゼロにします。
—
チーフアーキテクトからの提言
コードレビューで汚い `switch-case` 文を見かけたら、単に「リファクタリングして」と言うのではなく、こう伝えてください。
> 「Dart 3のswitch式と論理orパターン (`|`) を使って、ミュータブルな変数を排除し、コンパイル時の網羅性チェックが効く宣言的なコードに書き換えてください」と。
言語の進化に追従し、そのモダンな機能(セマンティクス)を骨の髄まで理解して使い倒すこと。それこそが、バグの温床を断ち切り、プロダクトの寿命を延ばす唯一の王道です。
次のプルリクエストから、ぜひこのテクニックを導入し、チーム全体のコード品質を次のステージへと引き上げてください。