みなさん、こんにちは!Dartの世界へようこそ。
FlutterやDartでアプリケーションを開発していると、一度は「`try-catch` を書いたのに、予想外の例外(Exception)がすり抜けてアプリがクラッシュしてしまった…」という苦い経験をしたことがあるのではないでしょうか?
一般的な `try-catch` によるエラーハンドリングは、「どの関数が、どんな例外を投げるのか」が型定義を見ただけでは分からないという大きな弱点があります。
しかし、安心してください! Dart 3で導入された「パターンマッチング」と「`sealed` クラス」を活用すると、RustやSwiftのようなモダン言語と同等の「型安全なResult型」を自分たちの手で美しく実装できるようになります。
この記事を読み終わる頃には、例外処理に対する恐怖心が消え、「Dartのエラーハンドリング、なんて綺麗なんだ!」と感動していただけるはずです。ここをクリアすれば、Dartの基本と応用文法はバッチリマスターできますよ。一緒に学んでいきましょう!
—
1. なぜ従来の `try-catch` では不十分なのか?
まずは、従来の例外処理が抱えていた問題をイメージ図で整理してみましょう。
【従来の try-catch】
関数呼び出し ──(成功)──> 戻り値 T
│
└──(失敗)──> [暗黙の例外 Exception] 💥 アプリ側がキャッチし忘れるとクラッシュ!
【Result型を使ったアプローチ】
関数呼び出し ──> 戻り値 Result
│
└── Dart 3 パターンマッチングで「網羅的」に分岐!(漏れがあるとコンパイルエラー)
従来の `try-catch` は、型システム(静的型チェック)の「外側」でエラーが飛び交うため、開発者がドキュメントを読み込んで手動で `try-catch` を書く必要がありました。
一方、成功と失敗を1つの型で表現する `Result型` を使うと、エラーの存在が「型(シグネチャ)」として明示されます。そしてDart 3のパターンマッチングを使えば、エラー処理の書き忘れをコンパイラが自動で検知してくれるようになるのです。
—
2. Dart 3で作る「完全型安全なResult型」の実装
では、さっそく Dart 3 の新機能である `sealed` クラスを使って、最高の `Result` 型を組み上げてみましょう!
以下のコードを自分のエディタに貼り付けて試してみてください。
/// 成功(Success)または失敗(Failure)のいずれかを表現するResult型
/// `sealed` を指定することで、Dartコンパイラはこのクラスのサブクラスをすべて把握できます
sealed class Result {
const Result();
}
/// 成功を表すクラス
final class Success extends Result {
final S value;
const Success(this.value);
}
/// 失敗を表すクラス
final class Failure extends Result {
final E exception;
const Failure(this.exception);
}
💡 ここがDart 3の凄さ!(コードの深掘り解説)
1. `sealed class`
`sealed` 修飾子がついたクラスは、同じファイル内(ライブラリ内)でしか継承できません。これにより、Dartコンパイラは「`Result` のサブクラスは `Success` と `Failure` の2つしか存在しない」という確信(網羅性)を持ちます。
2. `final class`
サブクラスに `final` を付けることで、外部のコードでこれ以上勝手に継承されるのを防ぎ、型階層を強固に固定します。
3. ジェネリクス ``
`S` は Success(成功時のデータの型)、`E` は Exception(失敗時のエラーの型)を表します。
—
3. パターンマッチングと `switch` 式による感動的なエラー処理
作成した `Result` 型を使って、実際の処理を書いてみましょう。ユーザーIDからユーザー名を取得する擬似的なAPI処理を考えてみます。
// 独自の例外クラスを定義
class UserNotFoundException implements Exception {
final String message;
UserNotFoundException(this.message);
}
// Result型を返す関数
Result
if (userId == 1) {
return const Success(‘Dart達人’); // 成功!
} else {
return Failure(UserNotFoundException(‘ユーザーID: $userId が見つかりませんでした。’)); // 失敗!
}
}
void main() {
final result = fetchUserName(2);
// Dart 3 の「switch 式」と「オブジェクトパターン」を活用!
final message = switch (result) {
// Success型であり、その中の value を取り出す(デストラクチャリング)
Success(value: final name) => ‘🎉 取得成功: $nameさん’,
// Failure型であり、その中の exception を取り出す
Failure(exception: final e) => ‘⚠️ 取得失敗: ${e.message}’,
};
print(message);
// 実行結果: ⚠️ 取得失敗: ユーザーID: 2 が見つかりませんでした。
}
🌟 パターンマッチングのポイント
- `switch` が「文」ではなく「式」になっている:
`final message = switch (result) { … };` のように、`switch` 全体が値を返すため、変数に直接代入できます。コードが非常にスッキリしますよね!
- オブジェクトパターンによる分解(デストラクチャリング):
`Success(value: final name)` という記述により、「`Success` 型かどうか」をチェックすると同時に、内部の `value` フィールドをそのまま変数 `name` として抽出しています。
- 省略記法(フィールド変数のショートハンド):
ちなみに `Success(:final value)` と書くことで、プロパティ名と同じ変数名で受けることも可能です。
—
4. コンパイラが守ってくれる「網羅性チェック(Exhaustiveness Check)」
ここが今回一番お伝えしたいDart 3の真骨頂です!
もし、以下のように `Failure` のハンドリングをうっかり忘れてしまったとしましょう。
// ❌ 開発者がFailureの処理を忘れてしまったコード
final message = switch (result) {
Success(value: final name) => ‘🎉 取得成功: $nameさん’,
// Failure のケースを書き忘れた!
};
従来のDartであれば、実行するまでエラーに気づけませんでした。
しかし、Dart 3ではコンパイルの段階で Dart CFE (Common Front End) が静的解析を行い、即座に以下のような赤線(コンパイルエラー)を出してくれます。
> コンパイルエラーの例:
> `The type ‘Result
「`Failure` の場合が考慮されていませんよ!」と、Dartが優しく教えてくれるのです。これで不注意によるバグは100%防止できますね!
—
5. 初学者が陥りやすい文法エラーと対策
このパターンを使い始めた方がよく引っかかる罠とその解決策を共有しておきますね。
罠1:ジェネリクスの型推論が効かずに型不一致になる
`Result` を返すときに、単に `Success(data)` と返すと、型が `Success
【対策】関数の戻り値に明示的な型をつけるか、ヘルパー関数を使う
// 関数の戻り値の型(Result
// Dartの強力な型推論(Type Inference)によって Success(user) だけで正しく型が合致します。
Result
return Success(User()); // OK!
}
罠2:非同期処理(`Future`)と組み合わせたときに混乱する
非同期処理で `Result` を返す場合は、`Future
Future
try {
// ネットワーク通信など
final response = await fakeApiCall();
return Success(response);
} on Exception catch (e) {
// 最下層のインフラ層でのみ try-catch を使い、例外を Result に変換して吸い上げる!
return Failure(e);
}
}
> 💡 先輩のアドバイス:
> アプリの「一番外側(API通信やファイル読み込みの最下部)」でだけ `try-catch` を使い、例外を `Result.failure` に変換してしまえば、それより上の「ビジネスロジック層やUI層」では 一切 `try-catch` を書く必要がなくなります。これぞ超・クリーンなアーキテクチャです!
—
6. まとめ
お疲れ様でした! Dart 3のパターンマッチングを使った `Result型` の魅力が伝わりましたでしょうか?
今回学んだポイントを振り返ってみましょう。
1. `try-catch` は例外が隠れてしまうが、`Result型` なら成功・失敗が型として明示される。
2. `sealed class` を使うことで、サブクラスのパターンを限定できる。
3. `switch` 式とパターンマッチングで、型のチェックと値の取り出し(分解)が同時に美しく書ける。
4. コンパイラの網羅性チェックにより、エラー処理の書き忘れが原理的に発生しなくなる。
この「Result型パターン」は、現代の言語設計におけるデファクトスタンダード(世界標準)になりつつある非常に強力な設計手法です。
これが使えるようになれば、あなたのDart/Flutterコードの品質と堅牢性は見違えるほど跳ね上がりますよ。ぜひ今日から自分のプロジェクトで試してみてくださいね。
もし分からない点があれば、いつでも聞いてください。Dartの基本はこれでバッチリマスターです!応援しています!