開発現場のコードレビューをしていると、いまだに命令型のアプローチでコレクションを構築しているコードを見かける。
// よくある冗長なコード
var list =
list.add(HeaderWidget());
if (isLoggedIn) {
list.add(UserMenuWidget());
} else {
list.add(LoginButtonWidget());
}
for (var item in rawItems) {
list.add(ItemWidget(item));
}
非効率だ。 一時的なミュータブルなリストを用意し、条件分岐やループを外部から `add` でねじ込むこの手法は、コードの可読性を落とすだけでなく、不必要な状態変異を許容し、イミュータビリティの恩恵を台無しにする。
Dartは言語仕様レベルで コレクション内での制御構文(Collection If / For) をサポートしている。これは単なる「糖衣構文」ではない。AST(抽象構文木)の構築段階から、宣言的UIフレームワーク(Flutterなど)やデータパイプラインの構築において、極めてエレガントかつ堅牢なバイトコード生成を行うためのアーキテクチャ上の重要な機能だ。
今回は、Dartのコレクションリテラル(List, Set, Map)における `if` と `for` の極限の活用法を、プロダクションコードの文脈で解説する。
—
なぜ命令型の `add` はバグを生むのか?
フロントエンド開発や非同期API連携において、UIコンポーネントや送信データのリストを動的に構築するケースは多々ある。ここで一時変数を使ったミュータブルな構築を行うと、以下のリスクが生じる。
1. スコープの汚染: 一時変数が不要に長生きする。
2. `const` 汚染と最適化の阻害: 実行時までリストが不確定になるため、AOTコンパイラによる定数畳み込み(Constant Folding)の恩恵を受けにくい。
3. 意図しない副作用: 条件分岐の複雑化に伴い、要素の順序が崩れる、あるいは重複追加されるバグの温床になる。
Dartのコレクションリテラル内での `if` / `for` は、「式(Expression)」 として評価される。つまり、コレクションの定義そのものがひとつの不変な文脈として完結する。
—
実践:プロダクションコードにおける宣言的データ構築
APIから受け取った生データを加工し、UIコンポーネントのツリーへマッピングする実用的なコードを見てほしい。余計なヘルパーメソッドやミュータブルな変数の一切を排除した、保守性の高い設計だ。
import ‘package:flutter/material.dart’;
// APIからのDTO(Data Transfer Object)を想定
class ApiPayload {
final String title;
final bool isFeatured;
final List
const ApiPayload({
required this.title,
required this.isFeatured,
required this.tags,
});
}
class DashboardBuilder {
/// 堅牢かつ宣言的にウィジェットツリーを構築するメソッド
List
required ApiPayload? payload,
required bool isMaintenanceMode,
required List
}) {
return [
// 1. 静的なヘッダー要素
const HeaderWidget(),
// 2. メンテナンスモード時の緊急バナー(条件付き挿入)
if (isMaintenanceMode)
const EmergencyBanner(message: ‘System under maintenance.’)
else if (payload == null)
const SkeletonLoader()
else …[
// 3. 複数の要素をスプレッド演算子 (…) と組み合わせる
TitleWidget(title: payload.title),
// 4. フラグに応じた条件付きコンポーネント
if (payload.isFeatured)
const BadgeWidget(label: ‘FEATURED’),
// 5. コレクション内の for ループによる動的要素の展開
for (var tag in payload.tags)
ChipWidget(tagText: tag),
// 6. 権限に応じた管理者専用メニューの展開(条件付きループ)
if (userPermissions.contains(‘admin’))
for (var action in [‘Purge Cache’, ‘Force Sync’, ‘Restart VM’])
AdminActionTile(actionName: action),
],
// 7. フッター
const FooterWidget(),
];
}
}
このコードのアーキテクチャ的優位性
- スプレッド演算子 (`…`) との完璧な融和: 条件が真のときに、単一の要素だけでなく、複数の要素(リスト)をシームレスに展開できる。
- 網羅性の高い条件分岐: `if-else if-else` チェーンがコレクション内できれいに完結するため、UIのロジックが視覚的に追いやすい。
- スコープの完全な分離: ループ変数(`tag`, `action`)は、そのコレクションリテラルのスコープ内に完全に閉じ込められており、外側を汚染しない。
—
Mapリテラルにおける `if` / `for` の真価
Listだけでなく、Mapの構築においてもこの機能は強力だ。特に、APIへ送信するペイロード(JSON)を動的に組み立てる際、`null` チェックを行ってからキーを追加するような冗長なコードを駆逐できる。
Map
required String userId,
required String? nickname,
required List
bool includeMetadata = false,
}) {
return {
‘user_id’: userId,
// nullでなければキーを含める(ボイラープレートの排除)
if (nickname != null) ‘nickname’: nickname,
// リストが存在し、かつ空でない場合のみ展開
if (selectedRoles != null && selectedRoles.isNotEmpty)
‘roles’: [
for (var role in selectedRoles) role.toUpperCase(),
],
// 条件に応じたメタデータの埋め込み
if (includeMetadata) …{
‘client_version’: ‘2.1.0’,
‘platform’: ‘Dart VM / Flutter’,
‘timestamp’: DateTime.now().toIso8601String(),
},
};
}
このMap構築手法により、「とりあえず空のMapを作って、後から `if (foo != null) map[‘foo’] = foo;` とミュータブルに詰めていく」という悪習を完全に断ち切ることができる。コンパイラに対しても「このデータ構造は初期化時点で形状が確定する」というヒントを与え、メモリ上の最適化を促すことになる。
—
パフォーマンス上の注意点とDart VMの挙動
チーフアーキテクトとして、パフォーマンスに関する重要な事実を伝えておく。
1. AOTコンパイルと定数 (`const`):
コレクションリテラル内に `if` や `for` が含まれている場合、原則としてそれは実行時評価(Runtime Evaluation)となる。静的な `const` リストにはできないため、極めて頻繁に(例えば60fps/120fpsのフレーム描画ごとに)巨大なリストをこれで再構築するのはメモリ割り当て(Allocation)の観点から避けるべきだ。
- 対策: ウィジェットツリーの粒度を適切に分割し、変化しない部分は `const` コンストラクタで切り出し、動的な部分のファイナライズにのみ `if`/多様なループを活用すること。
2. O(N) の評価コスト:
ネストされた `for` ループをコレクション内で行う場合、当然ながら計算量は $O(N \times M)$ となる。データソースの規模が大きい場合は、コレクションリテラル内で直接ループを回すのではなく、事前にストリームや非同期処理、あるいは別途最適化されたメソッドでデータを整形(Mapやfilter)してから渡すべきである。
—
まとめ
Dartのコレクション内 `if` と `for` は、単なるコード量を減らすためのシュガーシンタックスではない。それは「イミュータブル(不変)なデータ構築を強制し、コードの意図を宣言的に表現するための強力なパラダイムシフト」である。
コードレビューでミュータブルな一時リストや `add` の嵐を見つけたら、こう問いかけてほしい。
> 「そのリスト、宣言的に書けないのか?」と。
この記述方法をチーム標準とし、堅牢で美しいプロダクションコードベースを築き上げてほしい。