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

こんにちは!FlutterやDartの開発現場で日夜コードを書いていますか?

アプリケーションを作っていると、避けて通れないのが「エラー処理」ですよね。皆さんは普段、エラーをどう扱っていますか?おそらく多くのコードベースでは、`try-catch`構文があちこちに散りばめられているのではないでしょうか。

// よくある try-catch の世界
try {
final user = fetchUser();
print(user);
} catch (e) {
// エラーの型が Object? になり、何が起きたのか実行時まで分からない…
print(‘エラー発生: $e’);
}

この「とりあえず `catch (e)` で受け止める」スタイル、実はコードの見通しを悪くするだけでなく、「どんなエラーが起きうるのか」をコンパイラが教えてくれないという大きな弱点を持っています。

今回は、Dart 3で導入された`sealed class`とパターンマッチングを組み合わせ、例外に頼らない型安全なエラーハンドリングの仕組み――「Result型」を一緒に作っていきましょう!

ここをクリアすれば、あなたのDartコードの堅牢性は一気に跳ね上がりますよ。さあ、一緒にマスターしていきましょう!

—

なぜ `try-catch` だけでは不十分なのか?

`try-catch` は強力ですが、Dartの文法上、`catch (e)` でキャッチされる例外(Exception)やエラー(Error)は基本的に `Object?` 型として扱われます。

つまり、「この関数を呼び出すと、ネットワークエラーとパースエラーの2種類が起きる可能性がある」という情報を、関数のシグネチャ(定義)から読み取ることができないのです。すべては実行時(Runtime)になってみないと分かりません。

これでは、大規模なアプリケーションになればなるほど、「あ、ここでこの例外をキャッチし忘れてアプリがクラッシュした!」というバグを生む温床になってしまいます。

そこで登場するのが「Result型」という発想

RustやSwiftなどのモダンな言語に触れたことがある方ならお馴染みですが、「処理が成功した(Success)」か「失敗した(Failure)」かを、戻り値の型として明示的に表現するアプローチです。

これをDart 3の強力な機能を使って実装してみましょう。

—

ステップ1:`sealed class` で Result型を定義する

まずは、成功と失敗を表現する入れ物である `Result` クラスを作ります。ここで鍵になるのが `sealed class`(シールドクラス) です。

`sealed` を使うと、「このクラスを継承できるのは、同じファイル内で定義されたサブクラスだけ」という制限をコンパイラに強制できます。これにより、後でパターンマッチングを行う際に「網羅性チェック(すべてのパターンを処理しているか)」が自動で行われるようになります。

// Result型は「成功(Success)」か「失敗(Failure)」のどちらかである
sealed class Result {
const Result();
}

// 成功を表すクラス(データ型 T を持つ)
final class Success extends Result {
final T value;
const Success(this.value);
}

// 失敗を表すクラス(エラー型 E を持つ)
final class Failure extends Result {
final E error;
const Failure(this.error);
}

> 先輩からのワンポイント解説:総称型(Generics)について
> `` という見慣れない記号がありますが、これは「成功した時のデータ型(T)」と「エラーの型(E)」を自由に入れ替えられるようにするためのものです。どんなデータでも扱える汎用的な箱を作っているイメージですね。

—

ステップ2:実際に Result型を使ってみる

では、この `Result` 型を返す関数を書いてみましょう。今回は例として、「ユーザーIDをもとにユーザー情報を取得する処理」を模擬します。

// 独自のカスタムエラー型を定義する(これも型安全の秘訣!)
sealed class UserError {}
class NetworkError extends UserError {}
class NotFoundError extends UserError {}

// ユーザーを取得する関数
Result fetchUserName(int id) {
if (id <= 0) { // 失敗のときは Failure で包んで返す return Failure(NotFoundError()); } if (id == 999) { // ネットワーク切断を想定 return Failure(NetworkError()); } // 成功のときは Success で包んで返す return Success('Dart 3 太郎'); } 例外を `throw` する代わりに、「正常系も異常系も、どちらも通常の戻り値として扱う」のがポイントです。関数を呼ぶ側は、エラーが起きる可能性をシグネチャから一目で知ることができます。

—

ステップ3:Dart 3 のパターンマッチングで美しくハンドリングする

さて、ここからが本番です!返ってきた `Result` 型を、Dart 3の `switch` 式(パターンマッチング)を使ってスマートに料理しましょう。

void main() {
final result = fetchUserName(1);

// Dart 3 の switch 式は、値として直接代入やreturnができる!
final message = switch (result) {
// Success型の場合:value を取り出す
Success(value: final name) => ‘ようこそ、$nameさん!’,

// Failure型の場合:エラーの種類(型)によって処理を分岐
Failure(error: NetworkError()) => ‘通信エラーが発生しました。接続を確認してください。’,
Failure(error: NotFoundError()) => ‘指定されたユーザーは見つかりませんでした。’,
};

print(message);
}

このコードの何がスゴいのか?

1. 網羅性チェック(Exhaustiveness Checking)
もし `UserError` に新しいエラー種類(例:`UnauthorizedError`)を追加した際、もし `switch` 側でその処理を書き忘れると、Dartのコンパイラが「処理されていないパターンがあります」と赤くエラーを出して教えてくれます。 実行時エラーを未然にコンパイル時に防げるわけです。
2. 安全なキャスト(型安全)
`case` の中で自動的にダウンキャスト(型の絞り込み)が行われるため、余計な `as` キャストや `is` チェックを書く必要が一切ありません。

—

陥りがちな文法エラーと注意点

初心者のうちは、Dart 3の新しいパターン構文で次のようなミスをしがちです。気をつけておきましょう!

  • `switch` 文と `switch` 式の書き間違い

今回使ったのは `final message = switch (result) { … }` という式(Expression)の形です。各ケースの末尾にカンマ `,` が必要になる点や、アロー構文 `=>` の右側にセミコロン `;` が来る点に注意してください(通常の `switch` 文とは少し構文が異なります)。

  • sealed クラスの定義ファイル分割

`sealed class` を継承するサブクラスは、原則として同じライブラリ(同じファイル、または `part` で結ばれたファイル群)内に定義する必要があります。別のパッケージから勝手に新しいエラー種類を継承させることができないため、安全なパターンマッチングが保証される仕組みになっています。

—

まとめ:例外駆動開発からの卒業

今回は、Dart 3の `sealed class` とパターンマッチングを駆使して、型安全な「Result型」を実装する方法を解説しました。

  • 例外(try-catch) は、想定外のシステム障害(ディスク容量不足やメモリ枯渇など)に使う。
  • Result型 は、業務ロジック上で起こりうるエラー(入力値不正、データ未存在、通信失敗など)を表現するために使う。

この使い分けができるようになると、あなたの書くDart / Flutterコードは、他の追随を許さないほど堅牢で美しいものに生まれ変わります。

「ここをクリアすれば、Dartの基本はバッチリマスターできますよ」!
ぜひ、今日のコードをご自身のプロジェクトのネットワーク層やデータ層にり入れて試してみてくださいね。次回の解説もお楽しみに!

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