【実務・中級編】Null安全と『Result型』の設計:例外を投げずにNull許容型を扱う関数型アプローチ – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Null安全の先へ:Result型で「例外とNull」の呪縛を解くアーキテクチャ

DartのSound Null Safetyは、Null参照エラーをコンパイル時に撲滅するための強力な武器だ。しかし、現場のコードを見てみると、`if (value != null)` のネストが地層のように積み重なり、UI層まで「Nullのハンドリング」が浸食している惨状をよく目にする。

真の堅牢な設計とは、Nullを「扱う」ことではなく、「型システムの境界内で制御し、例外という不透明なフローを型として明示化すること」にある。

今日は、Dartにおける `Result` パターンの実装と、それがなぜプロダクション環境で最強の防具となるのか、VMの挙動と型推論の観点から解き明かそう。

—

1. なぜ「例外」はNull安全と相性が悪いのか

Dartの `throw` は、型システムをバイパスする。関数のシグネチャ `Future fetchUser()` を見ても、そこに「どの例外が飛んでくるか」は定義されていない。開発者は「この関数は失敗するかもしれない」という不安を抱えながら、見えない地雷を避けるために `try-catch` を広範囲に貼り付ける。

これは、Dartが誇るSound Null Safetyの理念と矛盾する。「失敗する可能性がある」なら、それを関数の戻り値という「型」に含めるべきだ。

2. 実装:Result型の設計パターン

まずは、シンプルかつ強力な `Result` クラスを定義する。ここでの肝は、Dart 3.0から導入された `sealed class` を活用することだ。これにより、コンパイラが網羅性を保証してくれる。

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

/// 成功時のデータ
class Success extends Result {
final S value;
const Success(this.value);
}

/// 失敗時のエラー情報
class Failure extends Result {
final F error;
const Failure(this.error);
}

なぜこれが「美しい」のか

`sealed` を使うことで、`switch` 文によるパターンマッチング時に、開発者が `Success` と `Failure` の両方を処理し忘れると、コンパイラが即座にエラーを吐く。これはVMレベルで実行効率が良いだけでなく、「仕様の漏れ」をコード上で強制的に可視化できるという、フロントエンド開発における最強の防壁になる。

—

3. 実務での活用:非同期API連携の例

API通信の結果をハンドリングする場合、以下のように記述する。

// APIエラーを型として定義
enum ApiError { network, unauthorized, server }

// 戻り値に例外を投げず、Result型を返す
Future> fetchUserName(int id) async {
try {
final response = await http.get(…);
if (response.statusCode == 200) {
return Success(response.body);
}
return Failure(ApiError.server);
} catch (_) {
return Failure(ApiError.network);
}
}

// 利用側:ネストせず、宣言的に書く
void main() async {
final result = await fetchUserName(1);

final message = switch (result) {
Success(value: final name) => ‘Welcome, $name’,
Failure(error: final type) => ‘Error occurred: $type’,
};

print(message);
}

技術的ポイント:パフォーマンスへの影響

ここで生成されるオブジェクトは、Dart VMにおいて非常に軽量だ。Dartのコンパイラは、`sealed class` と `switch` 文の組み合わせを最適化し、メソッド呼び出しのオーバーヘッドを最小限に抑える(型チェックを分岐として展開する)。`try-catch` はスタックトレースの生成コストがあるが、`Result` は単なる値の受け渡しであり、高頻度で呼び出されるコンポーネント内でも極めて高速に動作する。

—

4. チーフアーキテクトからのアドバイス

実務において「Nullチェック地獄」に陥っているなら、以下の原則を徹底してほしい。

1. 「Null」は「値が存在しない」というビジネスロジックにのみ使う:
通信エラーや想定外のケースにNullを使ってはいけない。それは `Result` の領分だ。
2. 型推論を信じる:
`switch` 文内の `final name` のように、パターンマッチングと同時に変数を抽出する。これにより、スコープが限定され、安全性が飛躍的に高まる。
3. 例外を型に閉じ込める:
`catch` ブロックは、APIの境界線(リポジトリ層)だけで終わらせる。UI層にまで例外を持ち込ませないことが、保守性の高いアーキテクチャへの第一歩だ。

—

結びに:コードは「対話」である

今回紹介した `Result` パターンは、単なる記述の簡略化ではない。それは「この関数が失敗する可能性がある」という事実を、コードを読む全てのエンジニアに対して静的に伝えるドキュメントだ。

コードはコンピュータを動かすための命令であると同時に、チームメンバーに対する「設計の意思表示」である。Nullチェックに追われる日々を卒業し、型システムが守ってくれる快適で堅牢な開発環境を構築しよう。

Dartの可能性を最大限に引き出すのは、ライブラリではなく、君たちの「型に対する執着」だ。さあ、今すぐ既存のコードをリファクタリングしてくれ。