【入門編】DartのNull安全とジェネリクスの深い関係:ListとListの挙動の違い – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartでの開発を楽しんでいますか?

Dartの「Sound Null Safety(健全なNull安全)」は、実行時エラーの温床だった `null` によるクラッシュをコンパイル時に根絶してくれる強力な仕組みですよね。このおかげで、私たちは非常に堅牢なコードを書くことができるようになりました。

でも、ジェネリクス(総称型)と組み合わせたとたん、「あれっ、思った通りの型推論をしてくれない」「なぜここでエラーが出るの?」と頭を悩ませた経験はありませんか?

特に `List` と `List` の違いは、Dartの型システムの本質を理解するうえで絶対に避けて通れない重要ポイントです。ここをクリアすれば、あなたのDartの基本力は間違いなくワンランク上のレベルに到達しますよ!

今回は、ジェネリクスにおけるNull許容性の伝播ルールと、型安全なコレクション操作の極意を、優しく丁寧に紐解いていきましょう。

—

1. まずはおさらい:`T` と `T?` の決定的な違い

ジェネリクスの世界へ飛び込む前に、ベースとなる「Null非許容型」と「Null許容型」のおさらいです。

  • `T`(Null非許容型): 「絶対に `null` を入れないで!」という強い制約です。
  • `T?`(Null許容型): 「値が入るかもしれないし、`null` かもしれないよ」という許容の姿勢です。

これを普通の変数で考えると当たり前のように思えますが、これがクラスやコレクションの型引数(ジェネリクス)になると、途端に挙動が複雑化します。

—

2. `List` と `List` の決定的な違い:箱と中身のルール

ここからが本題です。`List` と `List` は、一見似ていますが、Dartの型システムにおいて全く異なる役割を持つ「別の生き物」として扱われます。

図解的にイメージしてみましょう。

  • `List`:

「この箱(リスト)自体も `null` にはできないし、中に入っている要素(T)も絶対に `null` にしてはいけない」という厳格なルール。

  • `List`:

「この箱自体は非Nullだけど、中に入っている要素(T?)は `null` を含んでもよい」という柔軟なルール。

実際のコードでその挙動の違いを見てみましょう。

void main() {
// 1. List (要素にnullを許さない)
List strictList = [‘Apple’, ‘Banana’];

// ❌ コンパイルエラー!
// strictList.add(null);
// 理由: ‘String’ 型のリストに ‘null’ は追加できません。

// 2. List (要素にnullを許す)
List flexibleList = [‘Apple’, null, ‘Banana’];

// ⭕️ これはOK!
flexibleList.add(null);
print(flexibleList); // 出力: [Apple, Banana, null, null]
}

「中身の要素に `null` を混ぜ込みたいなら、型引数に `?` をつけて `List` にしなければならない」——これがジェネリクスにおけるNull許容性伝播の基本ルールです。

—

3. 陥りやすい罠:型推論の「思わぬ親切心」とコンパイルエラー

開発現場で本当によくあるのが、「型推論に頼りすぎて、意図しない型になってしまうバグ」です。

次のコードを見てください。あなたなら、この変数 `items` の型は何になると推論されると思いますか?

void main() {
// nullを含んだリストをリテラルで定義してみる
var items = [‘Dart’, ‘Flutter’, null];

// さて、items の型は何でしょうか?
}

正解は、`List` です。
Dartのコンパイラは非常に優秀(親切)なので、リストリテラルの中に `null` が含まれていると、「あ、このリストの要素には `null` が入るんだな」と自動的に察知し、型を `List` に格上げ(あるいは格下げ?)してくれます。

では、逆に次のようなケースはどうでしょう?

T processItem(T item) {
// 何らかの処理をする関数だとします
return item;
}

ここで、`List` を受け取る関数を作ったとします。

void printListItems(List list) {
for (var item in list) {
// T が非Null型であることが保証されているため、
// itemに対して安全にStringのメソッドなどを呼び出せる
print(item.toString());
}
}

void main() {
List nullableList = [‘A’, null, ‘B’];

// ❌ ここでコンパイルエラーになります!
// printListItems(nullableList);
}

なぜエラーになるのか?

コンパイラはこう考えます。
「`printListItems` は `List`(中身が絶対に `null` でないリスト)を受け取るように設計されている。それなのに、渡された `nullableList` は `List`(中身が `null` かもしれないリスト)だ。型安全性が壊れてしまうから渡せないよ!」

このように、「親(リスト)の型引数 `T` が、子(要素)の `null` を許容するかどうか」の厳密な一致が求められるのが、DartのSound Null Safetyの真骨頂です。

—

4. 型安全なコレクション操作のための実践テクニック

「じゃあ、要素に `null` が混ざっているかもしれないリストから、安全に非Nullのデータだけを取り出したい時はどうすればいいの?」という疑問が湧きますよね。

ここで活躍するのが、Dartの強力なコレクションメソッドである `whereType()` や `where` です。

void main() {
List maybeNullList = [‘Dart’, null, ‘Flutter’, null, ‘Null Safety’];

// テクニック1: whereType() を使う
// これは指定した型(この場合はString)の要素だけを抽出し、
// 自動的に List (非Nullのリスト)として返してくれます!
List safeList = maybeNullList.whereType().toList();

print(safeList);
// 実行結果: [Dart, Flutter, Null Safety]
// ※ 型は List になり、もう null は含まれません!
}

`whereType()` は、内部でしっかりと型チェックを行ってくれるため、キャストの危険を冒すことなく、安全に `List` から `List` を作り出すことができる最高のユーティリティです。

—

まとめ

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

1. `List` と `List` は別物:箱の中身が `null` を許容するかどうかは、型引数の `?` が決定する。
2. 型推論の罠に注意:リストリテラルに `null` を含めると自動的に `List` になる。
3. 関数の引数渡しのミスマッチ:`List` を期待する関数に `List` は渡せない(逆も然り)。
4. `whereType()` を使いこなす:Null許容のコレクションから安全に非Nullのデータを取り出す最強の武器。

このジェネリクスとNull安全のルールを頭の片隅に置いておくだけで、コンパイルエラーに直面したときも「あ、今コンパイラは型とNullの安全性を守ろうとしてくれているんだな」とスッと原因が導き出せるようになります。

ここをクリアできれば、Dartの基本はもうバッチリマスターです!自信を持って、さらに高度なアーキテクチャやFlutterの開発に挑んでいきましょう。あなたのコーディングライフがより一層快適になることを応援しています!

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