【入門編】Dartの「Never」型と「Null」型の深淵:型システムの境界を理解する – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの開発現場で、日々コードと向き合っている先輩エンジニアです。

今回は、Dartの型システムにおける「底辺の住人」、そして型安全性を極限まで高めるためのキーパーソンである `Never` 型 と `Null` 型 について深く掘り下げていきたいと思います。

「他の言語からDartに来たけれど、なんか底なし沼みたいな型があるぞ…?」
「 `null` の扱いは分かったけど、`Never` っていつ使うの?」

そんな疑問を持っていませんか?ここをクリアすれば、あなたのDartの型に対する解像度は劇的に上がり、コンパイラを味方につけた強固なコードが書けるようになりますよ。
さあ、Dartの型階層の底の底、その深淵を一緒に覗いてみましょう!

—

1. Dart型階層の全体像:底辺に広がる「奈落」

Dartの型システムは、すべての型の祖先である頂点(`Object?`)から、最底辺の `Never` まで、美しく整えられたツリー構造(厳密にはラティス構造)をしています。

イメージしやすいように、型階層ピラミッドを文字で描いてみますね。

[ Object? ] <-- すべての型の親(Nullable) │ [ Object ] <-- nullを除外したすべての型の親 │ (各カスタムクラスやプリミティブ型: String, int など) │ [ Null ] <-- 値として「null」だけを持つ底辺一歩手前 │ [ Never ] <-- 「決して値を持たない」型システム最底辺の深淵 このピラミッドの底にある `Null` と `Never` は、一見地味ですが、Dart 3 のパターンマッチングや網羅性チェック(Exhaustiveness checking)において、コンパイラにコードの安全性を保証させるための絶対不可欠なピースなんです。

—

2. `Null` 型:存在しないことを表す「安全な不在」

まずは、おなじみの `Null` 型からおさらいしましょう。
Dart 2.12で導入された「健全なNull安全(Sound Null Safety)」により、変数はデフォルトで `null` を許容しません。

String name = “Dart”;
// name = null; <-- コンパイルエラー!String型にnullは入れられません もし `null` を許容したい場合は、型名の後ろに `?` をつけて `String?` のようにNullableにしますよね。このとき、型システム上に出現するのが `Null` 型です。

  • `Null` 型の正体: `null` という値「だけ」を許容する特別な型。
  • 階層の位置: すべての `Nullable` 型(`T?`)の底に位置し、あらゆる型の部分型(サブタイプ)になっています。

—

3. `Never` 型:コードの「絶対にそこへ到達しない」深淵

では、今回のメインテーマである `Never` 型 に進みましょう。名前からして何やら不穏ですが、非常にエリートな型です。

`Never` とは、「絶対に値が評価されないこと(=正常にここまで到達しないこと)」をコンパイラに伝えるための型です。

`Never` が使われる代表的なシチュエーション

1. 例外を投げる関数: 処理が途中で中断され、値を返さない関数。
2. 無限ループ: 決して終わらない処理。
3. 網羅性チェックのガード(後述): パターンマッチングで取りうる値がないことを示す。

コードで見てみましょう。

// この関数は「絶対に正常終了せず、例外を投げる」ため、戻り値の型は Never になります
Never throwError(String message) {
throw Exception(message);
}

void processValue(String? input) {
// input が null だったら即座に例外を投げる
// この時、throwError の戻り値が Never なので、
// コンパイラは「この下の行には絶対に到達しない」と確信します
String validValue = input ?? throwError(“値がnullです!”);

// ここに到達した時点で、input は絶対に String型であることが保証される!
print(validValue.length);
}

すごいですよね! `throwError` が `Never` を返すおかげで、Dartのコンパイラは「あ、この先は安全だな」と自動的に型を絞り込んでくれるのです。これが `Never` 型の持つ強力な魔法の力です。

—

4. Dart 3 パターンマッチングと `Never` による網羅性チェック

Dart 3で導入されたパターンマッチング(`switch` 式など)は、私たちの開発体験を劇的に変えました。ここで `Never` 型は、「すべてのケースを網羅したことの証明」として最高の仕事をしてくれます。

例えば、このようなカスタムステータスを考えてみましょう。

enum OrderStatus { pending, processing, completed }

String getMessage(OrderStatus status) {
return switch (status) {
OrderStatus.pending => ‘処理待ちです’,
OrderStatus.processing => ‘処理中です’,
OrderStatus.completed => ‘完了しました’,
// ここでもし completed を書き忘れたら、コンパイルエラーになります!
};
}

では、ここに「将来的に新しいステータスが増えるかもしれない、あるいは例外的な未知の値を弾きたい」というケースを想定し、デフォルトケース(`_`)を書いたとします。

ここで高度な型設計のテクニックが登場します。

String getMessageAdvanced(OrderStatus status) {
return switch (status) {
OrderStatus.pending => ‘処理待ちです’,
OrderStatus.processing => ‘処理中です’,
OrderStatus.completed => ‘完了しました’,

// もし万が一、網羅されていない値が入ってきた場合の処理
_ => handleUnknown(status),
};
}

// 未知の値を処理するヘルパー関数
Never handleUnknown(Object? value) {
throw ArgumentError(‘未知のステータスが検出されました: $value’);
}

このコード、非常に美しい構造をしています。
もし `OrderStatus` に新しい要素(例えば `cancelled`)が追加されたのに、`switch` 式のケースに追加し忘れたとしましょう。
その場合、コンパイラは「`cancelled` のときは `_` に流れて `handleUnknown(status)` が呼ばれるな。おっ、`handleUnknown` の戻り値は `Never` だから、このswitch式全体としての型落ちは問題ないな」と判断します。

しかし、「いや、enumのすべてのケースを直接 `switch` で明示的に網羅してほしい!」という厳格な設計をしたい場合はどうすればよいでしょうか?

ここに、Dartコアコミッターも愛用するイディオムがあります。

String getStrictMessage(OrderStatus status) {
return switch (status) {
OrderStatus.pending => ‘処理待ちです’,
OrderStatus.processing => ‘処理中です’,
OrderStatus.completed => ‘完了しました’,
// 故意に Never 型の変数にキャスト(あるいは代入)を試みる
// 万が一、網羅漏れがあれば、ここで型エラーになる
};
}

もし網羅漏れがある状態で、次のように書くとどうなるでしょう?

String getStrictMessage(OrderStatus status) {
return switch (status) {
OrderStatus.pending => ‘処理待ちです’,
OrderStatus.processing => ‘処理中です’,
// completed を書き忘れたとする

// 最後に残った型を Never にキャストしようとする
// status が OrderStatus.completed のままだと、completed は Never に代入できないためコンパイルエラーになる!
};
}

Dart 3のスイッチ式では、すべての分岐を網羅していない場合、残余の型(余った型)が `Never` 以外の型になります。そのため、`Never` 以外の型を返すパスが存在するとコンパイラが怒ってくれるのです。これにより、「将来enumに値が追加された時、対応漏れを100%コンパイル時に検知できる」という、極めて堅牢なコードベースが完成します。

—

まとめ:型システムの境界を味方につける

いかがでしたでしょうか? 今回のポイントを簡単にまとめておきますね。

1. `Null` 型は、値として `null` だけを持つ、すべてのNullable型のベース。
2. `Never` 型は、「絶対にそこに到達しない・値を返さない」ことを表す型システムの最底辺。
3. `Never` をうまく活用することで、例外処理やコードのフロー制御、そして Dart 3 のパターンマッチングにおける網羅性チェックを完璧にコントロールできる。

「ここをクリアすれば、Dartの基本はバッチリマスターできますよ!」と言った意味が、少し伝わったでしょうか?

一見難しそうに見える `Never` や `Null` の挙動も、Dart VMやコンパイラが「コードの安全性を証明するため」に用意してくれた強力な道具だと分かれば、怖くありませんよね。ぜひ、明日のコードから `Never` を意識した堅牢な設計を取り入れてみてください。

それでは、また次回の深淵でお会いしましょう!Happy Coding!

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