【入門編】DartのNull安全と「共変性(Covariance)」:Listが引き起こす型安全性の崩壊と対策 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは。Dartの深淵へようこそ。
FlutterでUIを構築しているとき、ふとした瞬間に「なぜこのコードはエラーにならないのか?」「なぜ実行時にクラッシュするのか?」と首を傾げたことはありませんか?

今日は、Dartの型システムにおいて、多くのエンジニアが沼に足を踏み入れる「共変性(Covariance)」と「Null安全」の危うい関係について、裏側のメカニズムを紐解いていきましょう。

—

1. 共変性(Covariance)とは何か?

まず、直感的なイメージを持ちましょう。
「犬は動物である」という継承関係があるとします。では、「犬のリスト(`List`)」は「動物のリスト(`List`)」だと言えるでしょうか?

直感的には「イエス」ですよね。この「型Aが型Bのサブタイプなら、リストAもリストBのサブタイプである」という性質を共変性と呼びます。

Dartにおける「緩やかな」共変性

Dartの`List`は、歴史的かつ実用的な理由から共変的に振る舞います。コードで見てみましょう。

class Animal {}
class Dog extends Animal {}

void main() {
List dogs = [Dog()];

// ここでListをListとしてアップキャスト(参照を広げる)
List animals = dogs;

// コンパイラは「animalsはAnimalのリストだ」と信じている
// しかし、中身はDogのリストであるという事実は隠蔽されている
animals.add(Animal()); // !!!ここで実行時エラー発生!!!
}

このコード、実は静的解析(コンパイル時)はパスします。しかし、実行すると`List`の中に`Animal`を入れようとした瞬間に、Dart VMが「型が違うよ!」と例外を投げて止まります。

2. なぜこれがNull安全を脅かすのか?

Null安全(Sound Null Safety)の目的は、「Nullであってはならない場所にNullが入ることをコンパイル時に防ぐ」ことです。しかし、先ほどの共変性の性質が、この「絶対的な防壁」に穴を開けることがあります。

例えば、`List`(Null非許容)を`List`(Null許容)として扱うケースを考えてみましょう。

void main() {
List nonNullList = [‘Hello’];

// 共変性により、List は List のサブタイプとして扱われる
List nullableList = nonNullList;

// nullableListは「Nullが入ってもいい」という契約を持っている
nullableList.add(null);

// すると、元の nonNullList に null が混入してしまう!
print(nonNullList[1]); // null が出力される(型安全の崩壊)
}

これは非常に危険です。本来「Nullが入らない」と保証されているはずのリストに、背後からこっそりとNullが忍び込んでいるのですから。これこそが、Dartが「Sound(健全)」であるために最も警戒しているポイントです。

3. なぜDartはこの挙動を許しているのか?

「じゃあ、厳格に禁止すればいいじゃないか」と思いますよね?
実は、これを完全に禁止すると、既存のDartエコシステム(特に古いライブラリや柔軟なUI構築)がほとんど動かなくなってしまうのです。

Dartチームは、「利便性」と「厳格性」のバランスを取るために、このような挙動をあえて許容し、その代わりに実行時チェックを強化することで全体を制御しています。

—

4. 現場で「型安全の崩壊」を防ぐための対策

では、私たちはどう書けばいいのでしょうか? 3つの鉄則を伝授します。

対策①:不変(Immutable)なコレクションを活用する

可能な限り`List`をそのまま公開せず、`UnmodifiableListView`などを使って、後から要素を追加できない形に制限しましょう。

import ‘dart:collection’;

void main() {
final List list = [‘Dart’];

// 書き込みを禁止することで、共変性の副作用を封じ込める
final unmodifiableList = UnmodifiableListView(list);
}

対策②:ジェネリクスの型を明示的に絞る

`dynamic`や`Object`に逃げるのは、型安全を捨てるのと同義です。可能な限り具体的な型を使い、`List`のような曖昧な定義を避けてください。

対策③:Collection if / for の活用

FlutterのUI構築でよく使う`[if (condition) item]`などの構文は、構築時に型を確定させやすいため、予期せぬ共変性の事故を防ぐのに非常に有効です。

—

まとめ:Dartの重みを理解する

Dartの型システムが「共変性」を許しているのは、単なる手抜きではなく、柔軟なプログラミング体験を支えるための「必要な遊び」です。

しかし、その「遊び」を理解せずに扱うと、Null安全という強固な防壁を自ら破壊することになります。

  • 「Listは共変である」という前提を忘れないこと。
  • 「型アップキャスト」を安易に行わないこと。
  • 不変性を活用し、データの汚染を防ぐこと。

この3つを意識するだけで、あなたの書くコードは一気にプロフェッショナルなレベルへと引き上げられます。Dartの型システムをマスターするということは、VMがどう動くかを想像する力を身につけるということ。

さあ、次はどんな複雑なロジックを攻略しましょうか?あなたの疑問が、また新たなDartの真理を照らしてくれるはずです。

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