【実務・中級編】Dartのパターンマッチングで「Result型」を実装し、例外処理を型安全にする – Dart コア文法・オブジェクト指向・Null安全解析バイブル

例外という名の「隠れた副作用」を捨てろ:Dart 3 パターンマッチングで実現する堅牢なResult型設計

Dartの`try-catch`は、一見便利だが、実は「関数シグネチャによる契約」を破壊する爆弾だ。メソッドが何を投げるか(Throw)を静的に解析するのは不可能であり、呼び出し側は常に「見えないエラー」に怯えなければならない。

Dart 3のパターンマッチングは、単なる構文糖衣ではない。これは、「エラーを型として戻り値に含め、呼び出し側に明示的に処理を強制する」という関数型プログラミングのパラダイムを、Dartの型システムに持ち込むための強力な武器だ。

今回は、プロダクション環境で例外を排除し、堅牢性を担保する「Result型」の実装と、その裏側にあるコンパイラレベルの最適化について解説する。

—

1. なぜ「例外」は避けるべきなのか

例外は「予期せぬ事態」には適しているが、APIのレスポンスやユーザー入力のバリデーションといった「業務上想定される失敗」には不向きだ。

  • 型安全性の欠如: `Future`と書かれていても、実際には例外でクラッシュする可能性がある。
  • 制御フローの破綻: `try-catch`を多用するとコードの可読性が下がり、`catch`を忘れた瞬間にアプリは死ぬ。

これらを解決するために、`Result`型(成功値かエラー値のどちらか片方のみを持つコンテナ)を導入する。

—

2. 究極のResult型実装:sealed class と switch 式

Dart 3の `sealed class` を使えば、コンパイラが「全てのパターンが網羅されているか(Exhaustiveness checking)」を厳密にチェックしてくれる。これこそが、バグを未然に防ぐ最強の防波堤だ。

/// 成功と失敗を表現するResult型
sealed class Result {
const Result();
}

class Success extends Result {
final T value;
const Success(this.value);
}

class Failure extends Result {
final E error;
const Failure(this.error);
}

// 使用例:API通信のシミュレーション
Result fetchUserData(int id) {
if (id <= 0) { return Failure(Exception('Invalid ID: $id')); } return Success('User: DartMaster'); } ---

3. パターンマッチングによる「強制力のある」ハンドリング

この設計の肝は、呼び出し側が `switch` 式を使って処理を強制される点にある。

void main() {
final result = fetchUserData(-1);

// コンパイラが Success と Failure の両方を処理したか厳密にチェックする
final message = switch (result) {
Success(value: final val) => ‘Success: $val’,
Failure(error: final err) => ‘Error occurred: ${err.toString()}’,
};

print(message);
}

ここが技術的ポイント:
もし将来的に `Loading` や `Unauthorized` といった状態を `Result` に追加した場合、コンパイラは `switch` 式でそれらを処理していないと即座に警告を発する。つまり、「修正漏れによるクラッシュ」が物理的に発生しなくなる。

—

4. パフォーマンスの真実:なぜこれが高速なのか

「ラップされたオブジェクトを生成するのはオーバーヘッドではないか?」という疑問を持つ鋭い諸君へ。

1. アロケーションの最適化: Dart VMのAOTコンパイラは、`sealed class`による型階層を静的に理解している。`switch`式は、単なる条件分岐ではなく、VM内部で高度に最適化されたジャンプテーブルへとコンパイルされるため、実行時のオーバーヘッドは極めて小さい。
2. インライン化: パターンマッチングが単純な場合、コンパイラは該当する処理を呼び出し元にインライン展開する。例外を投げてスタックトレースを生成するコストと比較すれば、Result型によるオブジェクト生成コストなど誤差に近い。

—

5. 実務で「勝つ」ための運用戦略

プロダクションでResult型を導入する際は、以下のルールを徹底してほしい。

  • 例外を「境界」で塞ぐ: 外部ライブラリが投げる例外は、アプリのドメイン層に入る直前でキャッチし、`Failure`に変換する。これ以降、アプリの内部で `try-catch` が出現することは禁止する。
  • Resultを拡張する: `Result`に `map` や `when` といったメソッドを追加すれば、さらに流れるようなUI構築が可能になる。
  • Isolateとの相性: `Result`は単なるデータクラスであるため、Isolate間でのメッセージ受け渡し(SendPort)の際にもシリアライズが容易(`freezed`パッケージとの併用を推奨する)。

まとめ

Dart 3のパターンマッチングは、単なる構文の進化ではない。それは、「予測不能なランタイム」を「制御可能な型システム」へと押し込めるための設計革命だ。

コードレビューにおいて、もし誰かが `try-catch` を広範囲で使っていたら、それは「例外という名の隠れた副作用」を放置しているサインだ。この `Result` 型の設計パターンを注入し、チームのコードベースから「想定内のエラー」によるクラッシュを根絶せよ。

型安全とは、コンパイラを味方にすることだ。今日も最高にクリーンなコードを書いてくれ。

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