Dart コレクション — List / Set / Map 操作の実践
コレクションはデータの軍勢である — 順序付きリスト、重複排除セット、インデックス付きマップ。これらを組み合わせれば無敵になる。
1. 学べること
- List:作成 / 走査 / ソート / スプレッド(
...)とコレクション if/for 要素 - Set:重複排除と集合演算(積集合 / 和集合 / 差集合)
- Map:キー値操作と反復
- コレクションの型推論と const コレクション
- Bob のシナリオ:100 万件の注文のグループ化と集約
2. 開発者のリアルな物語
(1) 課題:手動のグループ化と集約は時間がかかりエラーが発生しやすい
Bob は 120 万件の注文をカテゴリ別にグループ化し、各カテゴリの総売上と注文数を計算する必要があった。彼は for ループでグループ化ロジックを手動で実装し、50 行のコードを書いたが、キー重複の上書きや不安定なソートなどの問題に遭遇した。120 万件の処理に 8 秒かかった。
(2) 解決策:コレクション操作
Dart のコレクション操作と高階関数を組み合わせると、グループ化と集約がわずか 10 行のコードになり、速度も 40% 向上した。
// カテゴリ別に注文をグループ化し集約
final byCategory = <String, List<Order>>{};
for (final order in orders) {
byCategory.putIfAbsent(order.category, () => []).add(order);
}
// または集約に fold を使う
final revenueByCategory = orders.fold<Map<String, double>>(
{}, (acc, o) => acc..update(o.category, (v) => v + o.amount, ifAbsent: () => o.amount));
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- コード量が 80% 削減され、ロジックがより明確に
- Set の重複排除が重複データを避け、Map のグループ化が手動インデックスに置き換わる
- スプレッドとコレクション if によりコレクション構築がより宣言的に
3. List
(1) 作成と基本操作
▶ サンプル:List の作成
void main() {
// リテラル作成
List<int> counts = [1, 2, 3, 4, 5];
var amounts = <double>[1500.0, 3200.0, 890.0];
// 拡張可能リスト
var orders = <String>['ORD-001', 'ORD-002'];
orders.add('ORD-003');
// 固定長リスト
var buffer = List<double>.filled(5, 0.0);
buffer[0] = 100.0;
// リストの生成
var batch = List.generate(10, (i) => 'Batch-${i + 1}');
print(counts); // [1, 2, 3, 4, 5]
print(orders); // [ORD-001, ORD-002, ORD-003]
print(buffer); // [100.0, 0.0, 0.0, 0.0, 0.0]
print(batch); // [Batch-1, ..., Batch-10]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:List の走査とソート
void main() {
var amounts = [1500.0, 890.0, 3200.0, 50.0];
// インプレースソート
amounts.sort();
print(amounts); // [50.0, 890.0, 1500.0, 3200.0]
// 降順ソート(新しいリストを作成)
var descending = [...amounts]..sort((a, b) => b.compareTo(a));
print(descending); // [3200.0, 1500.0, 890.0, 50.0]
// 要素へのアクセス
print(amounts.first); // 50.0
print(amounts.last); // 3200.0
print(amounts[2]); // 1500.0
// サブリスト
print(amounts.sublist(1, 3)); // [890.0, 1500.0]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:スプレッドとコレクション if/for
void main() {
var base = ['ORD-001', 'ORD-002'];
var extra = ['ORD-003', 'ORD-004'];
// スプレッド演算子
var combined = [...base, ...extra];
print(combined); // [ORD-001, ORD-002, ORD-003, ORD-004]
// null 認識スプレッド
List<String>? maybeList;
var safe = ['header', ...?maybeList, 'footer'];
print(safe); // [header, footer]
// コレクション if
bool includePremium = true;
var tiers = [
'Standard',
if (includePremium) 'Premium',
'Enterprise',
];
print(tiers); // [Standard, Premium, Enterprise]
// コレクション for
var batch = [
for (int i = 1; i <= 3; i++) 'Batch-$i',
];
print(batch); // [Batch-1, Batch-2, Batch-3]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 操作 | 構文 | 説明 |
|---|---|---|
| スプレッド | ...list |
要素を展開 |
| null 認識スプレッド | ...?list |
null 許容リストを展開 |
| コレクション if | if (cond) expr |
条件付き包含 |
| コレクション for | for (var x in list) expr |
ループ生成 |
4. Set
(1) 作成と集合演算
▶ サンプル:Set の重複排除と操作
void main() {
// リストから作成(重複を削除)
var allOrders = ['ORD-001', 'ORD-002', 'ORD-001', 'ORD-003'];
var uniqueOrders = <String>{...allOrders};
print(uniqueOrders); // {ORD-001, ORD-002, ORD-003}
// 集合演算
var setA = {1, 2, 3, 4, 5};
var setB = {4, 5, 6, 7, 8};
print(setA.intersection(setB)); // {4, 5} - 積集合
print(setA.union(setB)); // {1, 2, 3, 4, 5, 6, 7, 8} - 和集合
print(setA.difference(setB)); // {1, 2, 3} - 差集合
// メンバーシップテスト
print(setA.contains(3)); // true
print(setA.containsAll({1, 2})); // true
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:重複排除シナリオでの Set の応用
void main() {
// 注文リストからカテゴリを重複排除
var orders = [
{'id': 'ORD-001', 'category': 'Electronics'},
{'id': 'ORD-002', 'category': 'Books'},
{'id': 'ORD-003', 'category': 'Electronics'},
{'id': 'ORD-004', 'category': 'Clothing'},
{'id': 'ORD-005', 'category': 'Books'},
];
var categories = orders.map((o) => o['category'] as String).toSet();
print(categories); // {Electronics, Books, Clothing}
print('Unique categories: ${categories.length}'); // 3
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 操作 | メソッド | 結果 |
|---|---|---|
| 積集合 | a.intersection(b) |
共通要素 |
| 和集合 | a.union(b) |
全要素 |
| 差集合 | a.difference(b) |
a にあり b にない要素 |
| 含有 | a.contains(e) |
bool |
| 全含有 | a.containsAll(b) |
bool |
5. Map
(1) 作成と操作
▶ サンプル:Map の作成と基本操作
void main() {
// リテラル作成
var config = <String, dynamic>{
'appName': 'DataPipeline',
'maxRecords': 1000000,
'taxRate': 0.08,
'verbose': true,
};
// 値へのアクセス
print(config['appName']); // DataPipeline
print(config['missing']); // null
print(config['missing'] ?? 'N/A'); // N/A
// エントリの追加/更新
config['outputPath'] = '/tmp/reports'; // 追加
config['taxRate'] = 0.10; // 更新
// 安全な更新
config.update('maxRecords', (v) => v * 2, ifAbsent: () => 500000);
// 反復
config.forEach((key, value) => print(' $key: $value'));
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:Map の反復と集約
void main() {
var orders = [
{'id': 'ORD-001', 'category': 'Electronics', 'amount': 1500.0},
{'id': 'ORD-002', 'category': 'Books', 'amount': 50.0},
{'id': 'ORD-003', 'category': 'Electronics', 'amount': 3200.0},
{'id': 'ORD-004', 'category': 'Clothing', 'amount': 890.0},
{'id': 'ORD-005', 'category': 'Books', 'amount': 120.0},
];
// カテゴリ別にグループ化
final byCategory = <String, List<Map<String, dynamic>>>{};
for (final order in orders) {
final cat = order['category'] as String;
byCategory.putIfAbsent(cat, () => []).add(order);
}
// カテゴリ別に売上が集集約
final revenue = <String, double>{};
for (final entry in byCategory.entries) {
final total = entry.value.fold<double>(
0, (sum, o) => sum + (o['amount'] as double));
revenue[entry.key] = total;
}
// 結果を出力
revenue.forEach((cat, total) =>
print('$cat: \$${total.toStringAsFixed(2)} USD'));
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
6. コレクションの型推論と const コレクション
▶ サンプル:型推論と const コレクション
void main() {
// 型推論
var list1 = [1, 2, 3]; // List<int>
var list2 = [1, 2, 3.0]; // List<num> (int と double の混在)
var list3 = <String>[]; // 空リストの明示的ジェネリック
// const コレクション - 深くイミュータブル
const categories = ['Electronics', 'Books', 'Clothing'];
const config = <String, int>{'batchSize': 10000, 'timeout': 30};
const flags = {true, false};
// categories.add('Sports'); // エラー!const は変更不可
// クラス内の const
const defaultFormats = ['json', 'csv', 'html'];
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 機能 | var | final | const |
|---|---|---|---|
| 中身のミュータブル | はい | はい(参照はイミュータブル) | いいえ |
| 型推論 | 自動 | 自動 | 自動 |
| コンパイル時定数 | いいえ | いいえ | はい |
7. Bob のシナリオ:100 万件の注文のグループ化と集約
flowchart LR A[List: 120 万件の注文] --> B[where: フィルタ] B --> C[groupBy: カテゴリ] C --> D[Map: カテゴリ → 注文] D --> E[fold: 集約] E --> F[出力: 統計]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:実践的なグループ化と集約
class Order {
final String id;
final String category;
final double amount;
final String status;
Order({required this.id, required this.category, required this.amount, required this.status});
}
Map<String, Map<String, dynamic>> analyzeOrders(List<Order> orders) {
// 完了済み注文をフィルタ
final completed = orders.where((o) => o.status == 'completed').toList();
// カテゴリ別にグループ化
final grouped = <String, List<Order>>{};
for (final order in completed) {
grouped.putIfAbsent(order.category, () => []).add(order);
}
// カテゴリごとに集約
final result = <String, Map<String, dynamic>>{};
for (final entry in grouped.entries) {
final total = entry.value.fold<double>(0, (s, o) => s + o.amount);
final avg = total / entry.value.length;
result[entry.key] = {
'count': entry.value.length,
'total': total,
'average': avg,
};
}
return result;
}
void main() {
final orders = [
Order(id: 'ORD-001', category: 'Electronics', amount: 1500.0, status: 'completed'),
Order(id: 'ORD-002', category: 'Books', amount: 50.0, status: 'completed'),
Order(id: 'ORD-003', category: 'Electronics', amount: 3200.0, status: 'pending'),
Order(id: 'ORD-004', category: 'Clothing', amount: 890.0, status: 'completed'),
Order(id: 'ORD-005', category: 'Books', amount: 120.0, status: 'completed'),
];
final analysis = analyzeOrders(orders);
print('=== Category Analysis ===');
for (final entry in analysis.entries) {
final stats = entry.value;
print('${entry.key}:');
print(' Orders: ${stats['count']}');
print(' Total: \$${(stats['total'] as double).toStringAsFixed(2)} USD');
print(' Average: \$${(stats['average'] as double).toStringAsFixed(2)} USD');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. 完全なサンプル:DataPipeline コレクションアナライザー
// ============================================
// DataPipeline コレクションアナライザー
// List、Set、Map 操作の完全な実演
// ============================================
class Order {
final String id;
final String category;
final double amount;
final String status;
final String region;
const Order({
required this.id,
required this.category,
required this.amount,
required this.status,
this.region = 'US',
});
}
class OrderAnalyzer {
final List<Order> orders;
OrderAnalyzer(this.orders);
// ユニークなカテゴリ(Set)
Set<String> get categories => orders.map((o) => o.category).toSet();
// ユニークな地域(Set)
Set<String> get regions => orders.map((o) => o.region).toSet();
// 完了済み注文(List filter)
List<Order> get completed =>
orders.where((o) => o.status == 'completed').toList();
// 総売上(fold)
double get totalRevenue =>
completed.fold(0.0, (sum, o) => sum + o.amount);
// カテゴリ別売上(Map グループ化)
Map<String, double> get revenueByCategory {
return completed.fold<Map<String, double>>({}, (acc, o) {
acc.update(o.category, (v) => v + o.amount, ifAbsent: () => o.amount);
return acc;
});
}
// 地域とステータス別の注文数
Map<String, Map<String, int>> get countByRegionStatus {
final result = <String, Map<String, int>>{};
for (final order in orders) {
result.putIfAbsent(order.region, () => {});
result[order.region]!.update(
order.status, (v) => v + 1, ifAbsent: () => 1);
}
return result;
}
// 売上上位 N カテゴリ
List<MapEntry<String, double>> topCategories(int n) {
final sorted = revenueByCategory.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sorted.take(n).toList();
}
void printReport() {
print('=== DataPipeline Analytics Report ===');
print('Total orders: ${orders.length}');
print('Completed: ${completed.length}');
print('Unique categories: ${categories.length}');
print('Regions: ${regions}');
print('Total revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
print('\n--- Revenue by Category ---');
for (final entry in revenueByCategory.entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
print('\n--- Top Categories ---');
for (final entry in topCategories(3)) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
print('\n--- Orders by Region & Status ---');
for (final region in countByRegionStatus.entries) {
print(' ${region.key}: ${region.value}');
}
}
}
void main() {
final orders = [
Order(id: 'ORD-001', category: 'Electronics', amount: 1500.0, status: 'completed', region: 'US'),
Order(id: 'ORD-002', category: 'Books', amount: 50.0, status: 'completed', region: 'EU'),
Order(id: 'ORD-003', category: 'Electronics', amount: 3200.0, status: 'pending', region: 'US'),
Order(id: 'ORD-004', category: 'Clothing', amount: 890.0, status: 'completed', region: 'EU'),
Order(id: 'ORD-005', category: 'Books', amount: 120.0, status: 'completed', region: 'US'),
Order(id: 'ORD-006', category: 'Electronics', amount: 2100.0, status: 'completed', region: 'AP'),
];
OrderAnalyzer(orders).printReport();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Analytics Report ===
Total orders: 6
Completed: 5
Unique categories: 3
Regions: {US, EU, AP}
Total revenue: $4660.00 USD
--- Revenue by Category ---
Electronics: $3600.00 USD
Clothing: $890.00 USD
Books: $170.00 USD
--- Top Categories ---
Electronics: $3600.00 USD
Clothing: $890.00 USD
Books: $170.00 USD
--- Orders by Region & Status ---
US: {completed: 2, pending: 1}
EU: {completed: 2}
AP: {completed: 1}
❓ よくある質問
Q: List と Set のパフォーマンスの違いは何ですか? A: List は挿入順で要素を保持し、インデックスアクセスは O(1)、検索は O(n) です。Set はハッシュベースで、検索/含有チェックは O(1) ですがインデックスアクセスはできません。重複排除のシナリオでは Set を、順序が重要な場合は List を使います。
Q: Map のキーは任意の型にできますか? A: はい、ただしカスタムクラスをキーとして使う場合は
==とhashCodeをオーバーライドする必要があります。さもないと Map はエントリを正しく見つけられません。String か int をキーとして使うことを推奨します。
Q: 空の List には
[]と<Type>[]のどちらを使うべきですか? A: 型推論のコンテキストが利用可能なときは[]を使います。独立して宣言する場合は<Type>[]を使い、List<dynamic>として推論されるのを避けましょう。
Q: コレクション if とコレクション for は List リテラルでのみ使えますか? A: いいえ、List、Set、Map のリテラルはすべてコレクション if とコレクション for に対応しています。例:
{for (var i in items) i.name: i.value}。
Q: Map の
putIfAbsentとupdateの違いは何ですか? A:putIfAbsentはキーが存在しない場合のみ値を設定します。updateはキーが存在する場合のみ値を更新します(ifAbsentパラメータで非存在の場合を扱えます)。
Q: const コレクションと final コレクションの違いは何ですか? A: const コレクションはコンパイル時定数で、中身全体が完全にイミュータブル(深い不変性)です。final コレクションは参照がイミュータブルですが、中身はミュータブルです(例:final List には要素を追加できる)。
Q: 大きな List を効率的にソートする方法は? A:
List.sort()はインプレースでソートするため、[...list].sort()(コピーを作成する)より効率的です。元のリストを変更したくない場合は、まず[...list]でコピーを作成してからソートしてください。
📖 まとめ
- List は順序付き・重複可能で、スプレッド(
...)とコレクション if/for による宣言的構築に対応する。 - Set は順序なしで重複を防ぎ、O(1) 検索、積集合/和集合/差集合演算に対応する。
- Map はキー値ペアを保存し、
putIfAbsentとupdateがグループ化と集約の強力なツール。 - const コレクションは深くイミュータブル、final コレクションは参照はイミュータブルだが中身はミュータブル。
- 高階関数とコレクション操作の組み合わせにより、データ処理を 50 行からわずか 10 行に削減できる。
📝 練習問題
- 基礎(難易度 ⭐):10 件の注文金額を含む List を作成してください。
whereで 500 を超える金額をフィルタし、mapで USD にフォーマットし、reduceで合計を計算します。 - 中級(難易度 ⭐⭐):一連の注文データに対して、Map の
putIfAbsentとfoldを使ってカテゴリ別に注文をグループ化し、各カテゴリの注文数と総売上を計算してフォーマット済みレポートを出力してください。 - 挑戦(難易度 ⭐⭐⭐):
where、map、sortBy、groupBy、aggregateの連鎖操作をサポートするCollectionPipelineクラスを実装し、最終的に分析結果を出力してください。ヒント:各ステップは新しいコレクションまたは Map を返します。