こんにちは!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
final T value;
const Success(this.value);
}
// 失敗を表すクラス(エラー型 E を持つ)
final class Failure
final E error;
const Failure(this.error);
}
> 先輩からのワンポイント解説:総称型(Generics)について
> `
—
ステップ2:実際に Result型を使ってみる
では、この `Result` 型を返す関数を書いてみましょう。今回は例として、「ユーザーIDをもとにユーザー情報を取得する処理」を模擬します。
// 独自のカスタムエラー型を定義する(これも型安全の秘訣!)
sealed class UserError {}
class NetworkError extends UserError {}
class NotFoundError extends UserError {}
// ユーザーを取得する関数
Result
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の基本はバッチリマスターできますよ」!
ぜひ、今日のコードをご自身のプロジェクトのネットワーク層やデータ層にり入れて試してみてくださいね。次回の解説もお楽しみに!