Dart コレクション — List / Set / Map 操作の実践

コレクションはデータの軍勢である — 順序付きリスト、重複排除セット、インデックス付きマップ。これらを組み合わせれば無敵になる。

1. 学べること


2. 開発者のリアルな物語

(1) 課題:手動のグループ化と集約は時間がかかりエラーが発生しやすい

Bob は 120 万件の注文をカテゴリ別にグループ化し、各カテゴリの総売上と注文数を計算する必要があった。彼は for ループでグループ化ロジックを手動で実装し、50 行のコードを書いたが、キー重複の上書きや不安定なソートなどの問題に遭遇した。120 万件の処理に 8 秒かかった。

(2) 解決策:コレクション操作

Dart のコレクション操作と高階関数を組み合わせると、グループ化と集約がわずか 10 行のコードになり、速度も 40% 向上した。

DART
// カテゴリ別に注文をグループ化し集約
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));
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. List

(1) 作成と基本操作

▶ サンプル:List の作成

DART
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]
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:List の走査とソート

DART
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]
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:スプレッドとコレクション if/for

DART
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]
}
TEXT
> 出力: ローカルの 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 の重複排除と操作

DART
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:重複排除シナリオでの Set の応用

DART
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
}
TEXT
> 出力: ローカルの 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 の作成と基本操作

DART
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'));
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:Map の反復と集約

DART
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'));
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

6. コレクションの型推論と const コレクション

▶ サンプル:型推論と const コレクション

DART
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'];
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
機能 var final const
中身のミュータブル はい はい(参照はイミュータブル) いいえ
型推論 自動 自動 自動
コンパイル時定数 いいえ いいえ はい

7. Bob のシナリオ:100 万件の注文のグループ化と集約

100%
flowchart LR
  A[List: 120 万件の注文] --> B[where: フィルタ]
  B --> C[groupBy: カテゴリ]
  C --> D[Map: カテゴリ → 注文]
  D --> E[fold: 集約]
  E --> F[出力: 統計]
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:実践的なグループ化と集約

DART
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');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

8. 完全なサンプル:DataPipeline コレクションアナライザー

DART
// ============================================
// 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();
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== 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 の putIfAbsentupdate の違いは何ですか? A: putIfAbsent はキーが存在しない場合のみ値を設定します。update はキーが存在する場合のみ値を更新します(ifAbsent パラメータで非存在の場合を扱えます)。

Q: const コレクションと final コレクションの違いは何ですか? A: const コレクションはコンパイル時定数で、中身全体が完全にイミュータブル(深い不変性)です。final コレクションは参照がイミュータブルですが、中身はミュータブルです(例:final List には要素を追加できる)。

Q: 大きな List を効率的にソートする方法は? A: List.sort() はインプレースでソートするため、[...list].sort()(コピーを作成する)より効率的です。元のリストを変更したくない場合は、まず [...list] でコピーを作成してからソートしてください。


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):10 件の注文金額を含む List を作成してください。where で 500 を超える金額をフィルタし、map で USD にフォーマットし、reduce で合計を計算します。
  2. 中級(難易度 ⭐⭐):一連の注文データに対して、Map の putIfAbsentfold を使ってカテゴリ別に注文をグループ化し、各カテゴリの注文数と総売上を計算してフォーマット済みレポートを出力してください。
  3. 挑戦(難易度 ⭐⭐⭐)wheremapsortBygroupByaggregate の連鎖操作をサポートする CollectionPipeline クラスを実装し、最終的に分析結果を出力してください。ヒント:各ステップは新しいコレクションまたは Map を返します。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%