【実務・中級編】Dartの『Never』型を駆使した、Null安全なエラーハンドリングと網羅的チェック – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Null安全のその先へ:`Never`型でコンパイラを「共犯者」にするエラーハンドリング術

DartのSound Null Safetyは、単なる「Nullポインタ例外を防ぐためのガードレール」ではない。あれは、コンパイラと開発者が結ぶ「実行時エラーを撲滅するための契約」だ。

多くのエンジニアは`?`や`!`でNullを管理することに終始しているが、真に堅牢なDartコードを書く者は、型システムの最果てにある`Never`型を武器にする。`Never`とは、「決して値が存在しないこと」を型として表現するものだ。

今回は、この`Never`を使い、コンパイラをあなたのコードの監視役として強制的に働かせる高度なテクニックを伝授する。

—

1. `Never`型とは何か?:実行不可能を証明する型

Dartの型階層において、`Never`はすべての型のサブタイプだ。`Never`型の値は存在し得ない。関数が`Never`を戻り値として宣言するということは、「この関数は絶対に正常終了せず、例外を投げるか、無限ループするか、プロセスを終了させる」という絶対的な宣言を意味する。

// この関数は決して値を返さないことがコンパイラに保証される
Never throwError(String message) => throw Exception(message);

これがなぜ強力なのか? それは、`Never`を返す関数を呼び出した以降のコードは、コンパイラにとって「到達不可能」とみなされるからだ。

—

2. 網羅性チェック:`switch`文の「default」は怠慢である

多くの現場で見かけるのが、EnumやUnion(sealed class)を扱う際の「とりあえずの`default`」だ。

// 悪しき例:新しいステータスが増えてもコンパイラは何も教えてくれない
switch (status) {
case Status.active: return ‘Active’;
case Status.inactive: return ‘Inactive’;
default: return ‘Unknown’; // これがバグの温床
}

ここで`Never`を活用し、「未定義のケースはコンパイルエラーにする」という設計に書き換える。

// 改善例:Neverによる網羅的チェック
String statusToString(Status status) {
return switch (status) {
Status.active => ‘Active’,
Status.inactive => ‘Inactive’,
// ここで網羅性がチェックされる。新しいEnum値を追加すると、
// コンパイラが「残りのケースを処理しろ」と警告を発する。
};
}

もし、どうしても実行時に予期せぬ値が紛れ込む可能性がある(外部APIからの不整合など)なら、こうする。

String statusToString(Status status) {
return switch (status) {
Status.active => ‘Active’,
Status.inactive => ‘Inactive’,
// 網羅性チェックをパスしつつ、異常系をNeverで表現する
_ => throw ArgumentError(‘Unhandled status: $status’),
};
}

この`throw`は`Never`を返す。これにより、コンパイラは「この後の行は到達しない」と判断し、型推論の精度が極限まで高まる。

—

3. 実践:非同期API連携における堅牢なエラーハンドリング

実務で多用するAPIレスポンスのハンドリングを見てみよう。`sealed class`と`Never`を組み合わせれば、ビジネスロジックに「安全」を強制できる。

sealed class ApiResponse {}
class Success extends ApiResponse { final T data; Success(this.data); }
class Failure extends ApiResponse { final String error; Failure(this.error); }

// Neverを活用した強力なハンドリング
T handleResponse(ApiResponse response) {
return switch (response) {
Success(data: final data) => data,
Failure(error: final msg) => throw Exception(‘API Error: $msg’), // Neverを返す
};
}

// 利用側
void main() {
final response = fetchUser(); // ApiResponseを返す想定

// handleResponseが成功すれば必ずUser型が帰ってくることが保証されている
final user = handleResponse(response);
print(user.name);
}

この設計の肝は、`Failure`クラスが`ApiResponse`を継承している点だ。これにより、「失敗したレスポンスにはデータが存在しない」という事実を、型システムレベルでコンパイラに教え込んでいる。

—

4. パフォーマンスと哲学

`Never`を用いた設計は、単にバグを減らすだけではない。Dart VMは、到達不可能コードを最適化プロセスから即座に排除する。コードが整理されることで、AOTコンパイル時のバイナリサイズ削減や、実行時のキャッシュ効率向上にも微力ながら寄与する。

だが、真の恩恵はそこではない。「後からチームに加わった人間が、コードを書き換える際に、コンパイラからのエラーメッセージが設計意図を正しく伝えてくれる」という点にある。

  • `default`を避ける。
  • `sealed`クラスで状態を閉じ込める。
  • 異常系には`Never`を返し、フローを断ち切る。

これが、メンテナンス性に悩まされない、世界最高峰のDartコードを書くための作法だ。

—

今すぐできるアクションプラン

1. プロジェクト内の`switch`文を見直し、`default`ケースが「思考停止」になっていないか確認せよ。
2. `sealed class`を導入し、状態遷移を網羅的に強制せよ。
3. エラーハンドリング関数を整理し、戻り値が`Never`であることを明示的に宣言せよ。

型システムは敵ではない。コンパイラをあなたの「最も厳格で、最も優秀なコードレビュー担当者」へと育て上げろ。それが、Dartのチーフアーキテクトが贈る、真のプロフェッショナルへの道だ。

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