対象者
- Dartを使ったアプリケーション開発に携わるソフトウェアエンジニア
- コレクションの変換方法に関心がある開発者
- 業務効率を向上させ、プロジェクトで高い評価を得たいと考えている方
はじめに
プログラムの効率性を高め、コードの品質を向上させるためには、コレクションの変換は非常に重要です。しかし、変換方法に悩むことはありませんか?特に、初めてDartに触れる方にとっては、適切な変換方法を見つけるのは難しいかもしれません。
この記事では、具体的なコード例を交えながら、SetからListへの変換、ListからSetへの変換、MapからListへの変換、そしてListからMapへの変換方法を丁寧に解説します。最後にクラスへの変換とJSONからの変換を実施します。
さあ、この記事を読んで、Dartのコレクション変換の理解を深め、プロジェクトでの成功に一歩近づきましょう。
基本
Set、List、Mapの違い
-
Set:
- 重複する要素を持たないコレクション。
- 順序が保証されない。
- 要素の存在確認が高速。
-
List:
- 順序が保証されるコレクション。
- 重複する要素を許可。
- インデックスで要素にアクセスできる。
-
Map:
- キーと値のペアを持つコレクション。
- キーは重複を許さず、各キーは一意。
- キーを使って値に高速にアクセスできる。
Set -> List
Set
をList
に変換する基本的な方法を見てみましょう。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
次に、List
をSet
に変換する例です。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
次に、List
をMap
に変換する方法を見てみましょう。
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
クラスを使用して、List
とMap
の相互変換を行います。
共通部分
まず、共通部分として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
クラスのインスタンスを含むList
をMap
に変換する方法を見てみましょう。
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
次に、Map
をList
に変換する方法です。
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);
});
});
}