こんにちは。Dartの深淵へようこそ。
Dartの型システムは、一見すると直感的で親しみやすいものですが、その裏側には堅牢な「Sound Null Safety(健全なNull安全)」という強力な守護者が存在しています。
今日は、多くの開発者が一度は頭を抱える「共変性(Covariance)」と「リストの代入可能性」について、コンパイラが裏で何を考えているのかを紐解いていきましょう。ここを理解すれば、Dartの型安全性が「ただのルール」ではなく「実行時のバグを未然に防ぐ防壁」であることが腑に落ちるはずです。
—
1. なぜ `List` に `List` を代入できないのか?
まず、直感に反するこのコードを見てください。
List
// List
「`Object?` は `Object` の親分(スーパータイプ)なのだから、代入できていいじゃないか」と思いますよね。しかし、Dartのコンパイラはこれを許しません。なぜか?
それは、「後から `null` が混入するリスク」を排除するためです。
思考実験:もし代入できたら何が起きるか
もし仮に代入できてしまった場合、以下の恐ろしい事態が発生します。
List
List
nonNullableList.add(null); // 😱 大惨事!
// nonNullableListは「Object(nullは入らない)」と約束したはずなのに、
// 参照先であるnullableList経由でnullが紛れ込んでしまう。
`nonNullableList` を利用するコード側は、「ここには絶対 `null` が入っていない」と信じて処理を書きます。もしそこに `null` が混入すれば、実行時に `NullPointerException`(Dartでは `NoSuchMethodError`)が発生し、アプリはクラッシュします。
Dartは「推論の嘘」を許さない言語です。だからこそ、この代入をコンパイル時に断固拒否するのです。
—
2. 共変性(Covariance)の基本:それは「型が広がる方向」
共変性とは、簡単に言えば「型が継承関係に従ってそのままの方向に変換できる性質」のことです。
- `String` は `Object` のサブタイプです。
- したがって、`List
` は「なんとなく」 `List ` に代入できそうに見えます。
しかし、Dartのリストは「読み取り専用」ではありません。「書き込み(add)」ができてしまうのがポイントです。
読み込みだけなら安全、書き込みがあると危険
- 読み込み(Producer): `List
` を `List ` として扱うのは安全です。`String` は `Object` として振る舞えるからです。 - 書き込み(Consumer): `List
` を `List ` として扱ったら危険です。`Object`(数値など)を `String` のリストに突っ込めてしまうからです。
Dartはこの「書き込み」の危険性を考慮して、ジェネリクス型の代入においては「不変(Invariant)」という厳格なルールを採用しています。
—
3. どうすればいい?現場で役立つ回避策
では、このような制約がある中で、どうやって柔軟なコードを書くのでしょうか。賢い設計にはいくつかのパターンがあります。
パターンA:読み取り専用にする(Iterableを使う)
もしリストを読み取るだけであれば、`List` ではなく `Iterable` を使いましょう。`Iterable` は共変的であり、安全です。
List
Iterable
print(objects.first); // 読み取る分には安全だからです
パターンB:キャスト(as)で明示する
どうしても代入が必要な場合は、`List.cast
List
List
※ ただし、中身に `null` が含まれていれば実行時にエラーになるため、注意が必要です。
—
4. 最後に:Dartの型システムと仲良くなるために
DartのNull安全は、あなたを縛り付ける鎖ではありません。「実行時に発生する原因不明のNullエラー」という悪魔を、コンパイラという強力な盾で防いでくれる騎士なのです。
「なぜコンパイルエラーになるのか?」と悩んだときは、こう自問してみてください。
「この代入を許すと、あとでどこかのコードが『ここにnullは来ないはずだ』と油断して裏切られることにならないか?」
この視点を持てたとき、あなたはもうDartの型システムを掌握したも同然です。
ここをクリアすれば、Dartの基本はバッチリマスターできたと言っても過言ではありません。次は、より高度な「ジェネリクスの境界(Extends)」や「Mixin」の世界を覗いてみるのも面白いですよ。
それでは、素晴らしい開発ライフを!