【入門編】Dartのジェネリクス型パラメータにおける「extends」制約と、Null安全の相互作用 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

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

今回は、Dartのコア文法の中でも、多くの開発者が一度は躓き、そしてここを理解すると「Dartの型システムってめちゃくちゃよくできている!」と感動するテーマについてお話しします。

それは、「ジェネリクスの `extends` 制約と、Null安全(Null-safety)の相互作用」です。

「なんだか名前が難しそう…」と思いましたか? 大丈夫です!一歩ずつ、VM(Dart仮想マシン)の頭の中を覗くような気持ちで丁寧に紐解いていきますね。ここをクリアすれば、あなたのDartの型に対する解像度は劇的に上がりますよ。

—

1. 前提のおさらい:ジェネリクスの `extends` 制約とは?

まずは基本から。Dartでクラスや関数を作るとき、受け取る型を制限したい場面がありますよね。例えば、「数値型(`int` や `double`)だけを受け取れる箱を作りたい」といった場合です。

そんなときに使うのが `extends` キーワードです。

// T は num(またはそのサブタイプ)でなければならないという「制約」
class NumberBox {
final T value;
NumberBox(this.value);

// 2倍にして返すメソッド
num doubleValue() => value 2;
}

このコードでは、`T extends num` と書くことで、`T` には `num` か、それを継承した `int` や `double` しか入れられないようにコンパイル時にガードしています。`String` や `bool` を入れようとすると、Dartのコンパイラが「おいおい、それは `num` じゃないよ」と優しく怒ってくれます。

—

2. Null安全の世界へようこそ:ここで何が起きる?

さて、ここからが本題です。Dartは完全なNull安全(Sound Null Safety)を採用しています。つまり、デフォルトではすべての型は「非Null(Null非許容)」であり、明確に `?` をつけない限り、`null` を許しません。

では、先ほどの `extends num` の世界に、Null許容型(`?`付きの型)が飛び込んできたとき、コンパイラやDart VMの内部では何が起きるでしょうか?

次のコードを見てください。

// T は num?(num または null を許容する型)をextendsしている
class MaybeNumberBox {
final T value;
MaybeNumberBox(this.value);
}

ここで質問です。`T` が `num?` を `extends`(拡張)しているということは、`T` には `null` が入る可能性があるということになりますよね。

では、このクラスを使って次のインスタンスを作るとどうなるでしょう?

void main() {
// int? を指定してインスタンス化
var box = MaybeNumberBox(null);

// さて、この value を使って計算できるでしょうか?
// print(box.value 2); // ← コンパイルエラーになる?それとも…?
}

—

3. 境界条件の検証:`T extends num` と `T extends num?` の決定的な違い

Dartの型システムにおいて、`T extends num` と `T extends num?` は、見た目以上に大きな違いがあります。

イメージ図で考えてみましょう。

  • `T extends num` の世界

[ `num` ] ── 亜種( `int`, `double` )

  • `null` は絶対に存在できません。安心安全の「値だけ」の世界です。
  • `T extends num?` の世界

[ `num?` ] ── 亜種( `int`, `double`, `null` )

  • `null` が住むことを許された、少しカオスな世界です。

コンパイラは中でどう評価しているのか?

`MaybeNumberBox` の中で `value 2` を実行しようとすると、Dartのコンパイラは次のように考えます。

> 「おい、`T` は `null` かもしれないぞ。もし `value` が `null` だった場合、`null 2` なんて計算を実行したらRuntime Error(実行時エラー)になってしまう。だから、そのまま計算させるわけにはいかない!」

そのため、コンパイラは以下のようなエラーを出します。
`The operator ” isn’t defined for the type ‘T’.`(演算子 ” は型 ‘T’ に対して定義されていません)

—

4. どうやってこの境界を突破するのか?(実践的な解決策)

「じゃあ、Null許容なジェネリクス型を安全に扱いたいときはどうすればいいの?」と思いますよね。
答えは簡単です。Nullチェック(プロモーション)を行えばいいのです。

Dartの優秀なフロー解析(Flow Analysis)は、コードの文脈を読み取って型を安全に格上げ(プロモーション)してくれます。

class SafeNumberBox {
final T value;
SafeNumberBox(this.value);

num doubleValue() {
// value が null ではないことを Dart に教えてあげる
if (value == null) {
return 0; // null の場合のフォールバック
}

// ここに到達した時点で、Dartのコンパイラは
// 「T はもう null ではない(非Nullな num のサブタイプだ)」と判断します!
// そのため、キャストなしで安全に計算ができます。
return value 2;
}
}

void main() {
var boxWithNull = SafeNumberBox(null);
print(boxWithNull.doubleValue()); // 0 が出力される

var boxWithInt = SafeNumberBox(10);
print(boxWithInt.doubleValue()); // 20 が出力される
}

このコードでは、`if (value == null)` というガードを挟むことで、Dartの型チェッカーが賢く働き、`T` が持っていた「ひょっとしたら `null` かもしれない」という不安要素を見事に消し去ってくれます。これがDartのNull安全の真骨頂です。

—

まとめ:ここをクリアすればDartは怖くない!

今回のポイントをギュッと凝縮して振り返ってみましょう。

1. `T extends SomeType` は、その型の世界を「非Nullの範囲」に限定する強力なガード。
2. `T extends SomeType?` にすると、`null` が混入する可能性が生まれるため、メソッド内でそのまま演算やプロパティへのアクセスができなくなる。
3. 安全に使うためには、`if (value == null)` などのフロー解析を利かせるコードを書くことで、コンパイラに安全性を証明する必要がある。

ジェネリクスとNull安全の組み合わせは、一見すると複雑に感じられますが、「コンパイラに `null` の可能性をどう伝えるか」という視点を持つと、とてもシンプルに理解できます。

ここをクリアできれば、あなたもうDartの基礎はバッチリマスターしていますよ!ぜひ実際のコードでも試してみてくださいね。それでは、また次回の技術解説でお会いしましょう!

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