序文:その「推論」が、実行時の爆弾になっていないか?
Dartの型システムは、一見すると心地よく、親切に振る舞う。しかし、多くのエンジニアが「`var`で書けば勝手に良しなにしてくれる」という甘い幻想に抱かれ、実行時の`TypeError`という名の地獄に叩き落とされるのを、私は嫌というほど見てきた。
コレクション(List, Map, Set)は、Dartにおけるデータ流通の血管だ。ここでの型定義の甘さは、アプリケーション全体の堅牢性を致命的に損なう。コンパイラがどう型を決定し、Dart VMがそれをどうメモリに配置するか。その裏側を理解せずして、真にスケーラブルな設計は不可能だ。
今回は、コレクションリテラルの型推論とジェネリクスの深淵に踏み込み、実務で絶対に外してはならない「型安全の鉄則」を伝授する。
—
1. 型推論のメカニズム:上下方向の圧力
Dartの型推論は、単に右辺から左辺へ流れるだけではない。「ターゲット型(期待される型)」からのダウンワード推論と、リテラルの中身からのアップワード推論が交差する地点で決定される。
暗黙の `List` という敗北
以下のコードを見て、違和感を覚えないエンジニアは危険だ。
// 悪い例:型をコンパイラに丸投げしている
var data = [];
data.add(10);
data.add(“string”); // コンパイルは通るが、実行時に型安全性が崩壊する
この場合、`data` は `List
正しい推論の導き方
明示的に型を指定するか、初期値によってコンパイラに「制約」を与えよ。
// 良い例:アップワード推論を利用
final scores = [90, 85, 70]; // List
// より堅牢な例:ダウンワード推論(ターゲット型を明示)
final 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
List
// だが、これはランタイムエラーを引き起こす
try {
nums.add(1.5); // コンパイルは通るが、実行時に TypeError!
// なぜなら、実体は List
} catch (e) {
print(e);
}
}
この挙動を理解していないと、大規模なデータモデルの受け渡しで原因不明のクラッシュを招く。コレクションを外部に公開する際は、「読み取り専用」にするか、型を厳格に制限するパターンを徹底すべきだ。
—
4. 実務で使うべき「堅牢なコレクション設計」パターン
API連携やコンポーネント設計において、私がコードレビューで合格を出すレベルのコード例を示す。
パターンA:APIレスポンスの防御的パース
JSON(`Map
class UserProfile {
final List
UserProfile({required this.followers});
factory UserProfile.fromJson(Map
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
// 外部には不変のビューとして公開
// 読み取り専用であることを保証し、意図せぬ副作用を完全に遮断する
List
void fetchData() {
// 内部でのみ変更を行う
_internalData.addAll([1, 2, 3]);
}
}
—
5. パフォーマンスの最適化:リテラルの拡散演算子(Spread Operator)
Dart 2.3から導入された `…`(拡散演算子)と `if`/`for` コレクションは、単なるシンタックスシュガーではない。これらは命令的な `add()` 呼び出しよりも、宣言的にコレクションを構築でき、コンパイラによる最適化が効きやすい。
List
return [
‘Home’,
‘Settings’,
if (isAdmin) …[
‘Admin Panel’,
‘User Management’,
],
‘Logout’,
];
}
この書き方は、可読性だけでなく「最終的なリストのサイズ」をコンパイラが推測しやすく、内部的なバッファ確保の効率を高める。
—
まとめ:掌握せよ
Dartのコレクションは、ただの「入れ物」ではない。
1. 型を絞れ: `dynamic` を撲滅し、ダウンワード推論を戦略的に使え。
2. `const` を愛せ: メモリの正規化は、大規模アプリのパフォーマンスの生命線だ。
3. 共変性を疑え: `List
4. 不変性を強制せよ: `UnmodifiableListView` を使い、予期せぬ副作用をコードレビュー前に潰せ。
これが、Dart VMの性能を引き出し、数年後のメンテナンスに耐えうるコードを書くための、チーフアーキテクトからの助言だ。この重みを理解した上で、次の `List` を宣言してほしい。