【Dart】Set,List,Map,MapEntry,Jsonの変換を完全解説

  • 2024年7月9日
  • 2024年7月9日
  • Dart

対象者

  • Dartを使ったアプリケーション開発に携わるソフトウェアエンジニア
  • コレクションの変換方法に関心がある開発者
  • 業務効率を向上させ、プロジェクトで高い評価を得たいと考えている方

はじめに

プログラムの効率性を高め、コードの品質を向上させるためには、コレクションの変換は非常に重要です。しかし、変換方法に悩むことはありませんか?特に、初めてDartに触れる方にとっては、適切な変換方法を見つけるのは難しいかもしれません。

この記事では、具体的なコード例を交えながら、SetからListへの変換、ListからSetへの変換、MapからListへの変換、そしてListからMapへの変換方法を丁寧に解説します。最後にクラスへの変換とJSONからの変換を実施します。

さあ、この記事を読んで、Dartのコレクション変換の理解を深め、プロジェクトでの成功に一歩近づきましょう。

基本

Set、List、Mapの違い

  • Set:

    • 重複する要素を持たないコレクション。
    • 順序が保証されない。
    • 要素の存在確認が高速。
  • List:

    • 順序が保証されるコレクション。
    • 重複する要素を許可。
    • インデックスで要素にアクセスできる。
  • Map:

    • キーと値のペアを持つコレクション。
    • キーは重複を許さず、各キーは一意。
    • キーを使って値に高速にアクセスできる。

Set -> List

SetListに変換する基本的な方法を見てみましょう。Setは順序が保証されないコレクションですが、Listは順序が保証されます。以下のコードは、SetからListに変換し、順序を保証するためにソートする例です。

Set<int> numberSet = {1, 3, 2, 4, 5};
List<int> numberList = numberSet.toList();

expect(numberList, isA<List<int>>());
expect(numberList.length, equals(numberSet.length));
expect(numberList, containsAll(numberSet));

numberList.sort();
expect(numberList[0], 1);
expect(numberList[1], 2);
expect(numberList[2], 3);
expect(numberList[3], 4);
expect(numberList[4], 5);

上記のテストでは、まずSetからListに変換し、その後Listをソートして、正しい順序であることを確認しています。Setの特性上、順序が保証されないため、ソートが必要になる場合があります。

List -> Set

次に、ListSetに変換する例です。Listは重複を許しますが、Setは重複を許しません。以下のテストでは、Listから重複を除去してSetに変換しています。

List<int> numberList = [1, 2, 3, 4, 5, 5];
Set<int> numberSet = numberList.toSet();

expect(numberSet, isA<Set<int>>());
expect(numberSet.length, equals(5)); // 重複を除いた要素数
expect(numberSet, containsAll(numberList));

このテストでは、Listの重複要素がSetに変換される際に自動的に取り除かれることを確認しています。

Map -> List

次に、Mapからキーと値のListに変換する方法を見てみましょう。以下のテストでは、Mapのキーと値をそれぞれListに変換しています。

Map<String, int> numberMap = {'one': 1, 'two': 2, 'three': 3};
List<String> keysList = numberMap.keys.toList();
List<int> valuesList = numberMap.values.toList();

expect(keysList, isA<List<String>>());
expect(valuesList, isA<List<int>>());
expect(keysList, containsAll(['one', 'two', 'three']));
expect(valuesList, containsAll([1, 2, 3]));

このテストでは、Mapのキーと値を個別にListに変換して、それぞれのリストに正しい要素が含まれていることを確認しています。

List -> Map

次に、ListMapに変換する方法を見てみましょう。

for

List<int> numberList = [1, 2, 3];
Map<int, int> numberMap = {for (var i in numberList) i: i * 2};

expect(numberMap, isA<Map<int, int>>());
expect(numberMap.keys, containsAll(numberList));
expect(numberMap[1], 2);
expect(numberMap[2], 4);
expect(numberMap[3], 6);

forループを使ってListの各要素をMapに変換する方法です。この方法では、Listの要素をキーとして使用し、対応する値を設定します。

asMap

List<String> stringList = ['one', 'two', 'three'];
Map<int, String> numberMap = stringList.asMap();

expect(numberMap, isA<Map<int, String>>());
expect(numberMap.keys, containsAll([0, 1, 2]));
expect(numberMap[0], 'one');
expect(numberMap[1], 'two');
expect(numberMap[2], 'three');

asMapメソッドを使用してListのインデックスをキーとしてMapに変換する方法です。これにより、Listの要素が順序通りにMapに変換されます。

クラスを使った例

クラスを使ったより実践的な例を見てみましょう。Personクラスを使用して、ListMapの相互変換を行います。

共通部分

まず、共通部分としてPersonクラスを定義します。

class Person {
  const Person(this.id, this.name);
  final int id;
  final String name;

  @override
  int get hashCode => id.hashCode ^ name.hashCode;

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        (other is Person && name == other.name && id == other.id);
  }
}

const people = [
  Person(0, 'Anne'),
  Person(1, 'Bob'),
  Person(2, 'Charlotte'),
];

const map = {
  '0': 'Anne',
  '1': 'Bob',
  '2': 'Charlotte',
};

List -> Map

Personクラスのインスタンスを含むListMapに変換する方法を見てみましょう。

Map<int, String> map1 = {
  for (var person in people) person.id: person.name
};

Map<int, String> map2 = Map.fromIterable(
  people,
  key: (e) => e.id,
  value: (e) => e.name,
);

final map3 = Map.fromIterables(
    people.map((e) => e.id), people.map((e) => e.name));

expect(map1[0], 'Anne');
expect(map2[0], 'Anne');
expect(map3[0], 'Anne');

このテストでは、forループとMap.fromIterableを使用して、Listの各要素をMapに変換しています。

Map -> List

次に、MapListに変換する方法です。

final list = map.entries.map((e) => Person(int.parse(e.key), e.value)).toList();
expect(list, containsAll(people));

このテストでは、MapのエントリをListに変換し、元のPersonオブジェクトがすべて含まれていることを確認しています。

Jsonをクラスに変換する例

変換するデータ

    const json = '''
[
  { "group": 0, "id": 0, "name": "Anne" },
  { "group": 1, "id": 1, "name": "Bob" },
  { "group": 1, "id": 2, "name": "Charlotte" }
]''';
    final list = jsonDecode(json) as List<dynamic>;

先ほどのpeopleのデータと同じですが、JSONで元データがあるというユースケース用です。idとnameに加えて、groupを追加してます。それぞれのgroupごとにまとめたMapを作りたいと思います

json -> List

Jsonをエンコードすると、オブジェクトであればMap<String, dynamic>に変換されます。Jsonの上位がリストであればList<Map<String, dynamic>>になるので、それをクラスのリストに変換する方法を紹介します。

final result = list
  .map((e) => e as Map<String, dynamic>)
  .map((e) => Person(e['id'], e['name']))
  .toList();

expect(result[0].name, 'Anne');

このコードでは、Jsonデータの各エントリをPersonクラスのインスタンスに変換し、リストにまとめています。

json -> map: 通常

次に、Jsonデータを通常の方法でマップに変換する例です。ここでは、グループごとにPersonオブジェクトのセットを持つマップを作成します。

final groupSet = list.map((e) => e['group'] as int).toSet();
expect(groupSet.length, 2);

final map = <int, Set<Person>>{};
list.map((e) => e as Map<String, dynamic>).forEach(
    (e) => map
        .putIfAbsent(e['group'] as int, () => {})
        .add(Person(e['id'], e['name'])),
  );

expect(map[0]!.length, 1);
expect(map[1]!.length, 2);
expect(map[0]!.first.name, 'Anne');

このコードでは、各エントリをグループごとに分類し、それぞれのグループに対応するPersonオブジェクトのセットを作成しています。

json -> map: ワンライナー?(1行のプログラム)

最後に、Jsonデータをワンライナーでマップに変換する方法です。こちらもグループごとにPersonオブジェクトのセットを持つマップを作成しますが、コードを簡潔にまとめています。

Map<int, Iterable<Person>> map = {
  for (var group in list.map((e) => e['group']).toSet())
    group: list
        .where((e) => e['group'] == group)
        .map((e) => Person(e['id'] as int, e['name'] as String))
};

expect(map[0]!.length, 1);
expect(map[1]!.length, 2);
expect(map[0]!.first.name, 'Anne');

このワンライナーでは、各グループを一度に処理し、グループごとにPersonオブジェクトをリストからフィルタリングしてマップに格納しています。これにより、コードが簡潔で読みやすくなります、多分(理解しやすくなるとは言ってない)。

MapEntryでJsonを操作

MapEntryは、マップのキーと値のペアを表すクラスです。これを使うことで、マップのエントリを簡単に操作できます。以下に、MapEntryを使用した例を紹介します。

foldを使った例

foldを使用して、JsonデータをMapEntryに変換し、さらにPersonオブジェクトを含むマップに変換します。

final map = list
  .map((e) => MapEntry<int, Person>(
      e['group'], Person(e['id'] as int, e['name'] as String)))
  .fold(
    <int, List<Person>>{},
    (result, entry) => result
      ..update(entry.key, (list) => list..add(entry.value),
          ifAbsent: () => [entry.value]),
  );

このコードでは、foldを使って、各エントリをグループごとに分類し、Personオブジェクトを含むリストをマップに追加しています。

forを使った例

forループを使用して、MapEntryからPersonオブジェクトのマップを作成します。

final entries = list.map((e) => MapEntry<int, Person>(
 e['group'],
 Person(e['id'] as int, e['name'] as String),
));

Map<int, Iterable<Person>> map = {
 for (var group in entries.map((e) => e.key).toSet())
   group: entries
     .where((e) => e.key == group)
     .map((e) => e.value).toList()
};

このコードでは、forループを使用して各グループを処理し、Personオブジェクトをフィルタリングしてマップに格納しています。

groupをkeyにしたJsonでの変換

Jsonデータを扱う際に、キーとしてグループIDを使用し、値としてそのグループに属するオブジェクトのリストを持つケースはよくあります。ここでは、そのようなJsonデータをDartでどのように変換するかについて説明します。

以下は、グループIDをキーとして持つJsonデータを解析し、マップに変換するテストケースの例です。

Jsonデータの例

まず、グループIDをキーとし、そのグループに属するPersonオブジェクトのリストを持つJsonデータを示します。

const json = '''
{
  "0":[{ "id": 0, "name": "Anne" }],
  "1":[
    { "id": 1, "name": "Bob" },
    { "id": 2, "name": "Charlotte" }
  ]
}''';

Jsonデータの解析とマップへの変換

次に、このJsonデータを解析し、各グループIDをキーとし、そのグループに属するPersonオブジェクトのリストを持つマップに変換する方法を示します。

final map = jsonDecode(json) as Map<String, dynamic>;

test('json -> map', () {
  expect(map.keys.length, 2); // グループIDの数が2つであることを確認

  // マップのキーとしてグループIDを、値としてPersonオブジェクトのリストを持つマップを作成
  final result = Map.fromIterables(
    map.keys.map((key) => key), // キーをそのまま使用
    map.values.map((value) => 
        (value as List<dynamic>).map((e) => Person(e['id'], e['name']))), // 値をPersonオブジェクトのリストに変換
  );

  expect(result['0']?.length, 1); // グループID "0" に属するPersonオブジェクトの数が1であることを確認
  expect(result['0']?.first.id, 0);
  expect(result['0']?.first.name, 'Anne');
  expect(result['1']?.length, 2); // グループID "1" に属するPersonオブジェクトの数が2であることを確認
});

解説

  • Jsonデータの解析:

    • jsonDecode(json)を使用して、JsonデータをMap<String, dynamic>にデコードします。このマップのキーはグループIDで、値はそのグループに属するオブジェクトのリストです。
  • マップの生成:

    • Map.fromIterablesを使用して、キーと値のリストからマップを生成します。map.keysでキーのリストを、map.valuesで値のリストを取得します。
    • map.valuesの各値はList<dynamic>型であり、これをPersonオブジェクトのリストに変換するためにmap((e) => Person(e['id'], e['name']))を使用します。
  • テストの検証:

    • map.keys.lengthでグループIDの数が2つであることを確認します。
    • result['0']?.lengthでグループID "0" に属するPersonオブジェクトの数が1であることを確認します。
    • result['1']?.lengthでグループID "1" に属するPersonオブジェクトの数が2であることを確認します。

このようにして、グループIDをキーとし、各グループに属するオブジェクトを持つJsonデータを解析し、マップに変換することができます。これは、データを構造化して扱う際に非常に有用です。

まとめ

この記事では、Dartにおけるコレクションの変換について学びました。具体的には、SetからListへの変換、ListからSetへの変換、MapからListへの変換、そしてListからMapへの変換を詳しく見てきました。これにより、日常的なプログラミング作業で頻繁に使用するコレクション変換の理解が深まりました。

参考

ソース(main.dartにコピペして動作確認用)

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';

class Person {
  const Person(this.id, this.name);
  final int id;
  final String name;

  @override
  int get hashCode => id.hashCode ^ name.hashCode;

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        (other is Person && name == other.name && id == other.id);
  }
}

void main() {
  group('基本', () {
    test('Set -> List', () {
      Set<int> numberSet = {1, 3, 2, 4, 5};
      List<int> numberList = numberSet.toList();

      expect(numberList, isA<List<int>>());
      expect(numberList.length, equals(numberSet.length));
      expect(numberList, containsAll(numberSet));

      numberList.sort();
      expect(numberList[0], 1);
      expect(numberList[1], 2);
      expect(numberList[2], 3);
      expect(numberList[3], 4);
      expect(numberList[4], 5);
    });

    test('List -> Set', () {
      List<int> numberList = [1, 2, 3, 4, 5, 5];
      Set<int> numberSet = numberList.toSet();

      expect(numberSet, isA<Set<int>>());
      expect(numberSet.length, equals(5)); // 重複を除いた要素数
      expect(numberSet, containsAll([1, 2, 3, 4, 5]));
    });

    test('Map -> List', () {
      Map<String, int> numberMap = {'one': 1, 'two': 2, 'three': 3};
      List<String> keysList = numberMap.keys.toList();
      List<int> valuesList = numberMap.values.toList();

      expect(keysList, isA<List<String>>());
      expect(valuesList, isA<List<int>>());
      expect(keysList, containsAll(['one', 'two', 'three']));
      expect(valuesList, containsAll([1, 2, 3]));
    });

    group('List -> Map', () {
      test('for', () {
        List<int> numberList = [1, 2, 3];
        Map<int, int> numberMap = {for (var i in numberList) i: i * 2};

        expect(numberMap, isA<Map<int, int>>());
        expect(numberMap.keys, containsAll(numberList));
        expect(numberMap[1], 2);
        expect(numberMap[2], 4);
        expect(numberMap[3], 6);
      });
      test('asMap: 配列の順番がMapのKeyになる', () {
        List<String> stringList = ['one', 'two', 'three'];
        Map<int, String> numberMap = stringList.asMap();

        expect(numberMap, isA<Map<int, String>>());
        expect(numberMap.keys, containsAll([0, 1, 2]));
        expect(numberMap[0], 'one');
        expect(numberMap[1], 'two');
        expect(numberMap[2], 'three');
      });
    });

    group('クラスを使った例', () {
      const people = [
        Person(0, 'Anne'),
        Person(1, 'Bob'),
        Person(2, 'Charlotte'),
      ];

      const map = {
        '0': 'Anne',
        '1': 'Bob',
        '2': 'Charlotte',
      };

      test('List -> Map(idをkey, valueをnameで設定)', () {
        Map<int, String> map1 = {
          for (var person in people) person.id: person.name
        };

        Map<int, String> map2 = Map.fromIterable(
          people,
          key: (e) => e.id,
          value: (e) => e.name,
        );

        final map3 = Map.fromIterables(
            people.map((e) => e.id), people.map((e) => e.name));

        expect(map1[0], 'Anne');
        expect(map2[0], 'Anne');
        expect(map3[0], 'Anne');
      });

      test('Map -> list', () {
        final list =
            map.entries.map((e) => Person(int.parse(e.key), e.value)).toList();
        expect(list, containsAll(people));
      });
    });
  });

  group('json', () {
    const json = '''
[
  { "group": 0, "id": 0, "name": "Anne" },
  { "group": 1, "id": 1, "name": "Bob" },
  { "group": 1, "id": 2, "name": "Charlotte" }
]''';
    final list = jsonDecode(json) as List<dynamic>;

    test('json -> list', () {
      final result = list
          .map((e) => e as Map<String, dynamic>)
          .map((e) => Person(e['id'], e['name']))
          .toList();

      expect(result[0].name, 'Anne');
    });

    test('json -> map: 通常', () {
      final groupSet = list.map((e) => e['group'] as int).toSet();
      expect(groupSet.length, 2);

      final map = <int, Set<Person>>{};
      list.map((e) => e as Map<String, dynamic>).forEach(
            (e) => map
                .putIfAbsent(e['group'] as int, () => {})
                .add(Person(e['id'], e['name'])),
          );

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });

    test('json -> map: ワンライナー?', () {
      Map<int, Iterable<Person>> map = {
        for (var group in list.map((e) => e['group']).toSet())
          group: list
              .where((e) => e['group'] == group)
              .map((e) => Person(e['id'] as int, e['name'] as String))
      };

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });

    test('json -> map: foldを使って、グループ毎で実行', () {
      final map = list
          .map((e) => e['group'])
          .toSet()
          .toList()
          .fold(<int, Iterable<Person>>{},
              (Map<int, Iterable<Person>> result, groupId) {
        result[groupId] = list
            .where((person) => person['group'] == groupId)
            .map((person) =>
                Person(person['id'] as int, person['name'] as String));
        return result;
      });

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });

    test('json -> map: foldを使って、個人ごとで実行', () {
      final map = list.fold(<int, List<Person>>{},
          (Map<int, List<Person>> result, dynamic person) {
        final newPerson = Person(person['id'] as int, person['name'] as String);
        return result
          ..update(
            person['group'],
            (list) => list..add(newPerson),
            ifAbsent: () => [newPerson],
          );
      });

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });

    test('json -> map: MapEntryをfoldで操作', () {
      final map = list
          .map((e) => MapEntry<int, Person>(
              e['group'], Person(e['id'] as int, e['name'] as String)))
          .fold(
        <int, List<Person>>{},
        (result, entry) => result
          ..update(entry.key, (list) => list..add(entry.value),
              ifAbsent: () => [entry.value]),
      );

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });

    test('json -> map: MapEntryをforで操作', () {
      final entries = list.map((e) => MapEntry<int, Person>(
            e['group'],
            Person(e['id'] as int, e['name'] as String),
          ));

      Map<int, Iterable<Person>> map = {
        for (var group in entries.map((e) => e.key).toSet())
          group:
              entries.where((e) => e.key == group).map((e) => e.value).toList()
      };

      expect(map[0]!.length, 1);
      expect(map[1]!.length, 2);
      expect(map[0]!.first.name, 'Anne');
    });
  });
  
    group('json: グループをキーにしてみる', () {
    const json = '''
{
  "0":[{ "id": 0, "name": "Anne" }],
  "1":[
    { "id": 1, "name": "Bob" },
    { "id": 2, "name": "Charlotte" }
  ]
}''';
    final map = jsonDecode(json) as Map<String, dynamic>;

    test('json -> map', () {
      expect(map.keys.length, 2);
      final result = Map.fromIterables(
        map.keys.map((key) => key),
        map.values.map((value) =>
            (value as List<dynamic>).map((e) => Person(e['id'], e['name']))),
      );
      expect(result['0']?.length, 1);
      expect(result['0']?.first.id, 0);
      expect(result['0']?.first.name, 'Anne');
      expect(result['1']?.length, 2);
    });
  });
}