【実務・中級編】Dartのコレクションリテラル(List, Map, Set)の型推論とジェネリクスの挙動 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

序文:その「推論」が、実行時の爆弾になっていないか?

Dartの型システムは、一見すると心地よく、親切に振る舞う。しかし、多くのエンジニアが「`var`で書けば勝手に良しなにしてくれる」という甘い幻想に抱かれ、実行時の`TypeError`という名の地獄に叩き落とされるのを、私は嫌というほど見てきた。

コレクション(List, Map, Set)は、Dartにおけるデータ流通の血管だ。ここでの型定義の甘さは、アプリケーション全体の堅牢性を致命的に損なう。コンパイラがどう型を決定し、Dart VMがそれをどうメモリに配置するか。その裏側を理解せずして、真にスケーラブルな設計は不可能だ。

今回は、コレクションリテラルの型推論とジェネリクスの深淵に踏み込み、実務で絶対に外してはならない「型安全の鉄則」を伝授する。

—

1. 型推論のメカニズム:上下方向の圧力

Dartの型推論は、単に右辺から左辺へ流れるだけではない。「ターゲット型(期待される型)」からのダウンワード推論と、リテラルの中身からのアップワード推論が交差する地点で決定される。

暗黙の `List` という敗北

以下のコードを見て、違和感を覚えないエンジニアは危険だ。

// 悪い例:型をコンパイラに丸投げしている
var data = [];
data.add(10);
data.add(“string”); // コンパイルは通るが、実行時に型安全性が崩壊する

この場合、`data` は `List` と推論される。`dynamic` は静的解析の放棄であり、Dart VMにおける最適化の恩恵を自ら捨て去る行為だ。JIT/AOTコンパイラは、要素の型が特定できないリストに対して、要素アクセスのたびに高コストな型チェックを強制される。

正しい推論の導き方

明示的に型を指定するか、初期値によってコンパイラに「制約」を与えよ。

// 良い例:アップワード推論を利用
final scores = [90, 85, 70]; // List と確定

// より堅牢な例:ダウンワード推論(ターゲット型を明示)
final List tags = []; // 空でも List として扱われる

—

2. `const` コレクションと「正規化(Canonicalization)」

プロフェッショナルなら、`final` と `const` の違いを単なる「再代入の可否」で語ってはならない。コレクションにおける `const` は、Dart VMのメモリ管理における「正規化(Canonicalization)」を意味する。

void checkIdentity() {
final a = const [1, 2, 3];
final b = const [1, 2, 3];

print(identical(a, b)); // true: 同一のメモリ領域を参照している
}

`const` リテラルはコンパイル時に評価され、データセグメントの読み取り専用領域に配置される。何度呼び出しても同じインスタンスが再利用されるため、FlutterのWidgetツリーのような巨大な構造において、リビルドのコストを劇的に下げる。

テクニカルリードの視点:
APIから受け取った変更可能なリストを `const` にすることはできないが、UIの定数定義やデフォルト値には必ず `const` を使え。それは単なる不変性の保証ではなく、ヒープメモリの節約とGC(ガベージコレクション)負荷の軽減に直結する。

—

3. 共変性(Covariance)の罠:List は List か?

Dartのジェネリクスは「共変(covariant)」である。これが設計上の大きな落とし穴になる。

void main() {
List ints = [1, 2, 3];
List nums = ints; // 代入可能(共変)

// だが、これはランタイムエラーを引き起こす
try {
nums.add(1.5); // コンパイルは通るが、実行時に TypeError!
// なぜなら、実体は List であり double は入らないからだ
} catch (e) {
print(e);
}
}

この挙動を理解していないと、大規模なデータモデルの受け渡しで原因不明のクラッシュを招く。コレクションを外部に公開する際は、「読み取り専用」にするか、型を厳格に制限するパターンを徹底すべきだ。

—

4. 実務で使うべき「堅牢なコレクション設計」パターン

API連携やコンポーネント設計において、私がコードレビューで合格を出すレベルのコード例を示す。

パターンA:APIレスポンスの防御的パース

JSON(`Map`)からリストを生成する際、`cast()` を使ってはならない。`cast` はラップするだけで、要素アクセスのたびに型チェックを走らせるためパフォーマンスが劣悪だ。

class UserProfile {
final List followers;

UserProfile({required this.followers});

factory UserProfile.fromJson(Map json) {
return UserProfile(
// 悪い例: json[‘followers’].cast() -> 実行時のオーバーヘッドが大きい
// 良い例: 明示的に型を変換しながら新しいリストを生成する
followers: (json[‘followers’] as List? ?? [])
.map((e) => e.toString())
.toList(growable: false), // 不用意な追加を防ぐために不変にする
);
}
}

パターンB:`UnmodifiableListView` によるカプセル化

内部の状態をリストとして公開する場合、外部からの破壊的操作(`add` や `remove`)を型レベルで禁止せよ。

import ‘dart:collection’;

class DataRepository {
final List _internalData = [];

// 外部には不変のビューとして公開
// 読み取り専用であることを保証し、意図せぬ副作用を完全に遮断する
List get data => UnmodifiableListView(_internalData);

void fetchData() {
// 内部でのみ変更を行う
_internalData.addAll([1, 2, 3]);
}
}

—

5. パフォーマンスの最適化:リテラルの拡散演算子(Spread Operator)

Dart 2.3から導入された `…`(拡散演算子)と `if`/`for` コレクションは、単なるシンタックスシュガーではない。これらは命令的な `add()` 呼び出しよりも、宣言的にコレクションを構築でき、コンパイラによる最適化が効きやすい。

List buildMenu(bool isAdmin) {
return [
‘Home’,
‘Settings’,
if (isAdmin) …[
‘Admin Panel’,
‘User Management’,
],
‘Logout’,
];
}

この書き方は、可読性だけでなく「最終的なリストのサイズ」をコンパイラが推測しやすく、内部的なバッファ確保の効率を高める。

—

まとめ:掌握せよ

Dartのコレクションは、ただの「入れ物」ではない。

1. 型を絞れ: `dynamic` を撲滅し、ダウンワード推論を戦略的に使え。
2. `const` を愛せ: メモリの正規化は、大規模アプリのパフォーマンスの生命線だ。
3. 共変性を疑え: `List` に `Sub` を入れる際は、実行時の型安全性を設計レベルで担保せよ。
4. 不変性を強制せよ: `UnmodifiableListView` を使い、予期せぬ副作用をコードレビュー前に潰せ。

これが、Dart VMの性能を引き出し、数年後のメンテナンスに耐えうるコードを書くための、チーフアーキテクトからの助言だ。この重みを理解した上で、次の `List` を宣言してほしい。

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