【Dart】Set, List, Mapの操作法を徹底解説!メソッド網羅!

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

Table of Contents

対象者

  • Dartを使ったアプリケーション開発に取り組むソフトウェアエンジニア
  • Set、List、Mapの違いや操作方法を知りたい方
  • 効率的なデータ管理方法を学び、プロジェクトの進行をスムーズにしたい方

はじめに

Dartを使ったアプリケーション開発に取り組んでいる皆さん、こんにちは。プロジェクトの進行において、データ管理は重要な課題の一つですよね。特に、Set、List、MapといったDartのコレクションの使い方に悩んでいる方も多いのではないでしょうか。私自身、初めてこれらのデータ構造に触れたときは、それぞれの特性や使い分けに戸惑いました。しかし、正しく理解し使いこなすことで、コードのパフォーマンスを大きく向上させることができるのです。

この記事では、DartのSet、List、Mapの違いやそれぞれの操作方法について詳しく解説します。各データ構造の特性を理解し、最適な方法でデータを管理するスキルを身につけましょう。具体的なコード例も交えながら、実践的な内容を提供しますので、すぐにプロジェクトに応用できます。

Set,List,Mapの違い

各コレクション同士での変換は以下に記載しました。ご参照ください。

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

重複要素の扱い

Set、List、Mapはそれぞれ異なるデータ構造を持ち、重複要素の扱い方にも違いがあります。Setは重複を許さないデータ構造です。同じ要素が複数存在することはなく、追加された要素がすでに存在する場合は無視されます。Listは順序付きのコレクションで、同じ要素を何度でも追加できます。Mapはキーと値のペアを持つコレクションで、キーはユニークでなければならず、重複するキーが追加されると、後から追加された値で上書きされます。

例えば、以下のようなコードで重複要素の扱いを確認できます。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'apple'};
expect(fruitsSet, equals({'apple', 'banana'}));
// Listの場合
List<String> fruitsList = ['apple', 'banana', 'apple'];
expect(fruitsList, equals(['apple', 'banana', 'apple']));
// Mapの場合
Map<String, String> fruitsMap = {
  'a': 'apple',
  'b': 'banana',
  'a': 'apricot'
};
expect(fruitsMap, equals({'a': 'apricot', 'b': 'banana'}));

このように、Setは重複を排除し、Listは重複を許し、Mapはキーが重複すると上書きされるという特徴があります。これにより、データの特性に応じて適切なコレクションを選択することが重要です。

順序の保持

データの順序に関しても、Set、List、Mapはそれぞれ異なる特性を持ちます。Listは順序付きのコレクションで、要素は追加された順序を保持します。Setは一般的に順序を保証しませんが、挿入順序を保持するLinkedHashSetという実装も存在します。Mapはキーと値のペアを保持し、順序は保証されませんが、LinkedHashMapという実装を使えば、挿入順序を保持することができます。

要素のアクセス方法

要素へのアクセス方法もSet、List、Mapでは異なります。Listはインデックスを使用して要素にアクセスできます。これは、順序が保証されているためです。Setは一般的にインデックスを使ったアクセスは提供しておらず、要素の存在確認やイテレーションが主な操作となります。Mapはキーを使って値にアクセスします。キーは一意であり、対応する値を迅速に取得できます。

以下のコードは、それぞれのアクセス方法を示しています。

// Listの場合
List<String> fruitsList = ['apple', 'banana', 'cherry'];
expect(fruitsList[1], equals('banana'));

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
expect(fruitsSet.contains('banana'), equals(true));

// Mapの場合
Map<String, String> fruitsMap = {
  'a': 'apple',
  'b': 'banana',
  'c': 'cherry'
};
expect(fruitsMap['b'], equals('banana'));

このように、Listはインデックスで、Setは存在確認で、Mapはキーで要素にアクセスします。それぞれのコレクションの特性に応じたアクセス方法を理解し、適切に使用することが重要です。

Set,List,Mapの共通点

要素の追加

Set、List、Mapのいずれのコレクションも要素の追加が可能です。Setではaddメソッドを使って要素を追加しますが、重複する要素は無視されます。Listでは同じくaddメソッドを使用し、順序通りに要素が追加されます。Mapではキーと値のペアをputIfAbsentaddAllメソッドで追加します。

例えば、以下のように要素を追加できます。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana'};
fruitsSet.add('cherry');
expect(fruitsSet, equals({'apple', 'banana', 'cherry'}));

// Listの場合
List<String> fruitsList = ['apple', 'banana'];
fruitsList.add('cherry');
expect(fruitsList, equals(['apple', 'banana', 'cherry']));

// Mapの場合
Map<String, String> fruitsMap = {'a': 'apple', 'b': 'banana'};
fruitsMap.putIfAbsent('c', () => 'cherry');
expect(fruitsMap, equals({'a': 'apple', 'b': 'banana', 'c': 'cherry'}));

要素の追加はどのコレクションでも基本操作であり、データの構築に欠かせません。Setでは重複が自動的に排除され、Listでは順序が保持され、Mapではキーがユニークに管理されるため、用途に応じた選択が重要です。

要素の削除

要素の削除もSet、List、Mapの共通の機能です。Setではremoveメソッドを使用して指定した要素を削除します。ListではremoveremoveAtメソッドを使用して要素やインデックスを指定して削除できます。Mapではremoveメソッドで指定したキーとその値を削除します。

以下のコードは、要素の削除方法を示しています。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
fruitsSet.remove('banana');
expect(fruitsSet, equals({'apple', 'cherry'}));

// Listの場合
List<String> fruitsList = ['apple', 'banana', 'cherry'];
fruitsList.remove('banana');
expect(fruitsList, equals(['apple', 'cherry']));
fruitsList.removeAt(1);
expect(fruitsList, equals(['apple']));

// Mapの場合
Map<String, String> fruitsMap = {
  'a': 'apple',
  'b': 'banana',
  'c': 'cherry'
};
fruitsMap.remove('b');
expect(fruitsMap, equals({'a': 'apple', 'c': 'cherry'}));

要素の削除はデータのメンテナンスや更新に不可欠です。Setは特定の要素を、Listはインデックスや要素を、Mapはキーに基づいて削除できるため、それぞれの特性を理解して適切に利用することが重要です。

要素の存在確認

要素の存在確認もSet、List、Mapで共通の操作です。SetとListではcontainsメソッドを使って指定した要素が存在するかを確認します。MapではcontainsKeycontainsValueメソッドを使ってキーや値の存在を確認します。含まれている要素の数はlengthで確認できます。

以下の例で、要素の存在確認方法を示します。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
expect(fruitsSet.contains('banana'), equals(true));
expect(fruitsSet.length, 3);

// Listの場合
List<String> fruitsList = ['apple', 'banana', 'cherry'];
expect(fruitsList.contains('banana'), equals(true));
expect(fruitsList.length, 3);

// Mapの場合
Map<String, String> fruitsMap = {
  'a': 'apple',
  'b': 'banana',
  'c': 'cherry'
};
expect(fruitsMap.containsKey('b'), equals(true));
expect(fruitsMap.containsValue('banana'), equals(true));
expect(fruitsMap.length, 3);

要素の存在確認は、データの整合性チェックや特定の要素の有無を確認するために必要です。これにより、処理を行う前にデータの状態を確認し、エラーや不整合を防ぐことができます。

要素の繰り返し処理

Set、List、Mapのいずれのコレクションでも繰り返し処理が可能です。SetとListではfor-inループやforEachメソッドを使って要素を順に処理します。MapでもforEachメソッドを使い、キーと値のペアを順に処理します。
基本はfor-inループの使用が推奨されており、forEachは既に存在する関数(printなど)を呼び出しのみのようです。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
for (var fruit in fruitsSet) {
    expect(fruitsSet.contains(fruit), equals(true));
}

// Listの場合
List<String> fruitsList = ['apple', 'banana', 'cherry'];
for (var fruit in fruitsList) {
    expect(fruitsList.contains(fruit), equals(true));
}

for (int index = 0; index < fruitsList.length; index++) {
    var fruit = fruitsList[index];
    expect(fruitsList.contains(fruit), equals(true));
}

// Mapの場合
Map<String, String> fruitsMap = {
    'a': 'apple',
    'b': 'banana',
    'c': 'cherry'
};
for (var entry in fruitsMap.entries) {
    expect(fruitsMap.containsKey(entry.key), equals(true));
    expect(fruitsMap[entry.key], equals(entry.value));
}
for (var key in fruitsMap.keys) {
    expect(fruitsMap.containsKey(key), equals(true));
}
for (var value in fruitsMap.values) {
    expect(fruitsMap.containsValue(value), equals(true));
}

以下の例で、forEachでの繰り返し処理方法を示しますが推奨はされない。

// Setの場合
Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
fruitsSet
    .forEach((fruit) => expect(fruitsSet.contains(fruit), equals(true)));

// Listの場合
List<String> fruitsList = ['apple', 'banana', 'cherry'];
fruitsList
    .forEach((fruit) => expect(fruitsList.contains(fruit), equals(true)));

// Mapの場合
Map<String, String> fruitsMap = {
  'a': 'apple',
  'b': 'banana',
  'c': 'cherry'
};
fruitsMap.forEach((key, value) {
  expect(fruitsMap.containsKey(key), equals(true));
  expect(fruitsMap[key], equals(value));
});
fruitsMap.keys
    .forEach((key) => expect(fruitsMap.containsKey(key), equals(true)));
fruitsMap.values.forEach(
    (value) => expect(fruitsMap.containsValue(value), equals(true)));

// 以下は許される書き方
fruitsMap.entries.forEach(print);

繰り返し処理はデータの操作や出力において非常に有用です。各コレクションの特性に応じた繰り返し処理を行うことで、データの操作を効率的に行うことができます。

空のコレクションの作成と確認

Dartでは、空のコレクションを簡単に作成し、それらが空であるかどうかを確認するためのメソッドが用意されています。以下の例では、List、Set、Mapそれぞれに対して空のコレクションを作成し、その状態をテストしています。

// Listの空のコレクションの作成とテスト
final emptyList = <int>[];
expect(emptyList.isEmpty, equals(true));
expect(emptyList.isNotEmpty, equals(false));

// Setの空のコレクションの作成とテスト
final emptySet = <int>{};
expect(emptySet.isEmpty, equals(true));
expect(emptySet.isNotEmpty, equals(false));

// Mapの空のコレクションの作成とテスト
final emptyMap = <int, int>{};
expect(emptyMap.isEmpty, equals(true));
expect(emptyMap.isNotEmpty, equals(false));

// コレクションの型確認
expect([], isA<List>());
expect({}, isA<Map>());
expect({}, isNot(isA<Set>()));

このようにして、各コレクションが空であるかどうかを確認することができます。isEmptyプロパティはコレクションが空であるかを確認し、isNotEmptyプロパティはコレクションが空でないかを確認するために使用されます。これにより、データの存在を簡単にチェックすることができます。

空のSetを作成するときは、ジェネリックで型を指定するのが重要です。指定しないと、Setではなく、Mapとして作成されるので、混乱します。

Listの操作

Listの作成

ListはDartで最も基本的なコレクションの一つであり、順序を持つ複数の要素を保持するデータ構造です。Listは角括弧 [] を使用して作成され、要素をカンマ , で区切ります。

例えば、以下のようにListを作成します。

List<String> fruits = ["apple", "banana", "orange"];
List<int> numbers1 = [1, 2, 3, 4, 5];
var numbers2 = [1, 2, 3, 4, 5];
var numbers3 = <int>[1, 2, 3, 4, 5];
final numbers4 = <int>[1, 2, 3, 4, 5];

final dynamicList = [1, '2', 3, '4', '5'];

Listの連番の作成

Listを連番で簡単に作成するために、List.generateメソッドを使用することができます。List.generateは、指定された数の要素を生成するためのコールバック関数を受け取り、その関数に基づいて新しいListを作成します。これにより、任意のパターンや法則に従ったListを効率的に生成することができます。
また、Iterable.generateメソッドを使用することで、イテラブルな連番のコレクションを作成することもできます。

// List.generateを使用して連番のListを作成
final list = List.generate(3, (index) => index * 2);
expect(list, equals([0, 2, 4]));

// Iterable.generateを使用して連番のイテラブルを作成
final iterable1 = Iterable.generate(3);
expect(iterable1, equals([0, 1, 2]));

final iterable2 = Iterable.generate(3, (e) => e * 2);
expect(iterable2, equals([0, 2, 4]));

List.generateは、リストの各要素を生成するために使用する関数を提供することで、特定のパターンに従ったリストを簡単に作成できます。例えば、上記の例では、0から始まり、2ずつ増加する要素を持つリストを生成しています。
一方、Iterable.generateは、指定した数の要素を持つイテラブルを生成するために使用します。これにより、連番のコレクションを簡単に作成し、リストとして使用することができます。
これらのメソッドを使用することで、特定のパターンに従った連番のリストやコレクションを効率的に作成でき、プロジェクトにおけるデータ操作が容易になります。

要素のアクセス

List内の要素は0から始まるインデックスでアクセスできます。これにより、特定の位置にある要素を取得したり、変更したりすることが可能です。

例えば、以下のように要素にアクセスします。

List<String> fruits = ["apple", "banana", "orange"];
String firstFruit = fruits[0]; // "apple"
fruits[1] = "grape"; // "banana"を"grape"に変更
expect(fruits, equals(["apple", "grape", "orange"]));

インデックスを使うことで、特定の位置にある要素に直接アクセスできるため、Listは非常に操作しやすいデータ構造です。

要素の追加

ListにはaddメソッドやaddAllメソッド、insertメソッド、insertAllメソッドを使って要素を追加できます。
addメソッドは単一の要素を、addAllメソッドは別のListの最後に追加します。
insertメソッドは指定した位置に単一の要素を、insertAllメソッドは指定した位置に別のListを追加します。

例えば、以下のように要素を追加します。

 List<String> fruits = ["apple", "banana"];
fruits.add("orange");
expect(fruits, equals(["apple", "banana", "orange"]));

List<String> moreFruits = ["grape", "melon"];
fruits.addAll(moreFruits);
expect(fruits, equals(["apple", "banana", "orange", "grape", "melon"]));

fruits.insert(1, "orange");
expect(fruits,
  equals(["apple", "orange", "banana", "orange", "grape", "melon"]));

fruits.insertAll(2, ["grape", "melon"]);
expect(fruits, ["apple","orange","grape","melon","banana","orange","grape","melon"]);

これにより、リストの末尾や指定した位置に要素を追加することができます。addaddAllinsert、およびinsertAllメソッドを使うことで、簡単に要素を拡張できます。

要素の存在確認

Listには、要素の存在を確認するためのメソッドとしてcontainsany、およびeveryがあります。これらのメソッドは指定した条件に基づいてリスト内の要素の存在を確認します。

例えば、以下のように要素の存在を確認します。

bool hasApple = fruits.contains("apple"); // true
expect(hasApple, equals(true));

bool hasPear = fruits.contains("pear"); // false
expect(hasPear, equals(false));

List<int> numbers = [1, 5, 10];
bool anyGreaterThanFive = numbers.any((num) => num > 5);
expect(anyGreaterThanFive, equals(true));

bool allGreaterThanFive = numbers.every((num) => num > 5);
expect(allGreaterThanFive, equals(false));

これにより、特定の要素がリスト内に存在するかどうかを簡単に確認できます。containsメソッドは特定の要素の存在を、anyおよびeveryメソッドは条件に基づいた存在を確認します。

要素の変更

Listの要素はsetAllsetRangefillRange、およびreplaceRangeメソッドを使って変更できます。これにより、特定の範囲や全体の要素を一括で変更することができます。

例えば、以下のように要素を変更します。

List<String> fruits = ["apple", "banana", "orange"];
fruits.setAll(1, ["grape", "melon"]);
expect(fruits, equals(["apple", "grape", "melon"]));

fruits.setRange(0, 2, ["kiwi", "pineapple"]);
expect(fruits, equals(["kiwi", "pineapple", "melon"]));

fruits.fillRange(1, 3, "lemon");
expect(fruits, equals(["kiwi", "lemon", "lemon"]));

fruits.replaceRange(0, 2, ["strawberry"]);
expect(fruits, equals(["strawberry", "lemon"]));

これにより、リストの特定の範囲や全体を柔軟に変更することができます。特定の範囲を一括で変更することで、効率的にデータを操作できます。

要素の削除

Listの要素はremoveremoveAtremoveLastremoveRangeremoveWhereretainWhere、およびclearメソッドを使って削除できます。これにより、特定の要素や範囲を簡単に削除できます。

例えば、以下のように要素を削除します。

 List<String> fruits = ["apple", "banana", "orange", "grape"];
fruits.remove("banana");
expect(fruits, equals(["apple", "orange", "grape"]));

fruits.removeAt(1);
expect(fruits, equals(["apple", "grape"]));

fruits.removeLast();
expect(fruits, equals(["apple"]));
// fruits.removeFirst() はない

fruits.addAll(["melon", "lemon", "pear"]);
expect(fruits, equals(["apple", "melon", "lemon", "pear"]));

// インデックス1 から削除する。インデックス3 は削除しない
fruits.removeRange(1, 3);
expect(fruits, equals(["apple", "pear"]));

// 削除する条件を指定
fruits.removeWhere((fruit) => fruit.startsWith("p"));
expect(fruits, equals(["apple"]));

// 削除しない条件を指定
fruits = ['apple', 'pear'];
fruits.retainWhere((fruit) => fruit.startsWith('p'));
expect(fruits, ['pear']);

fruits.clear();
expect(fruits, equals([]));

removeWhereは条件に一致する要素を削除し、retainWhereは条件に一致する要素だけを残します。前者は不要な要素を排除し、後者は必要な要素のみを保持します。

これにより、特定の要素や範囲を柔軟に削除することができます。removeclearメソッドを使うことで、リストを効率的に管理できます。

要素の確認

Listの要素の確認は、forEach、elementAt、firstWhere、lastWhere、indexOf、indexWhere、lastIndexOf、lastIndexWhereなどのメソッドを使って行います。これらのメソッドは、特定の条件に基づいて要素を取得するために非常に便利です。

例えば、以下のように要素を確認できます。

forEach

forEachメソッドは、リストの全ての要素に対して処理を実行します。

List<String> fruits = ["apple", "banana", "orange"];
fruits.forEach((fruit) => expect(fruits.contains(fruit), equals(true)));

forEachメソッドは、全ての要素を処理するのに適しており、簡潔なコードを書くことができます。

elementAt

elementAtメソッドは、指定したインデックスの要素を返します。インデックスが範囲外の場合、エラーが発生します。

List<String> fruits = ["apple", "banana", "orange"];
String fruit = fruits.elementAt(1);
expect(fruit, equals("banana"));

elementAtメソッドは、特定の位置にある要素を取得するために使用されます。

where

whereメソッドは、リストの中で指定した条件に一致する全ての要素を返します。

List<int> numbers = [1, 2, 3, 4, 5];
final numbersWhere = numbers.where((num) => num < 4);
expect(numbersWhere.length, 3);

//Setも同様
expect({1, 2, 3, 4, 5}.where((num) => num < 4).length, 3);

whereメソッドは、条件に一致する要素をフィルタリングするのに便利で、結果として一致する全ての要素を含む新しいIterableを返します。

firstWhere

firstWhereメソッドは、リストの中で指定した条件に一致する最初の要素を返します。

List<int> numbers = [1, 2, 3, 4, 5];
int number = numbers.firstWhere((num) => num > 3);
expect(number, equals(4));

// 見つからない場合は例外
expect(() => [].firstWhere((num) => num > 3), throwsA(isA<StateError>()));

expect([].firstWhere((num) => num > 3, orElse: () => -1), -1);

firstWhereメソッドは、条件に一致する最初の要素を見つけるために非常に便利です。
該当する値がなかった場合例外が発生します。しかしorElseを指定することで見つからなかったときの値を指定することができます。

lastWhere

lastWhereメソッドは、リストの中で指定した条件に一致する最後の要素を返します。

List<int> numbers = [1, 2, 3, 4, 5];
int number = numbers.lastWhere((num) => num < 4);
expect(number, equals(3));

lastWhereメソッドは、条件に一致する最後の要素を見つけるために使用されます。

indexOf

indexOfメソッドは、指定した要素のインデックスを返します。要素が存在しない場合、-1を返します。

List<String> fruits = ["apple", "banana", "orange"];
int index = fruits.indexOf("banana");
expect(index, equals(1));

// 見つからない場合、-1を返す
expect(fruits.indexOf("pear"), -1);

indexOfメソッドは、特定の要素の位置を見つけるのに適しています。

indexWhere

indexWhereメソッドは、指定した条件に一致する最初の要素のインデックスを返します。

List<String> fruits = ["apple", "banana", "orange"];
int index = fruits.indexWhere((fruit) => fruit.startsWith("o"));
expect(index, equals(2));

indexWhereメソッドは、条件に一致する要素の位置を特定するのに便利です。

lastIndexOf

lastIndexOfメソッドは、指定した要素の最後のインデックスを返します。要素が存在しない場合、-1を返します。

List<String> fruits = ["apple", "banana", "orange", "banana"];
int index = fruits.lastIndexOf("banana");
expect(index, equals(3));

lastIndexOfメソッドは、リスト内で重複する要素の最後の位置を特定するのに役立ちます。

lastIndexWhere

lastIndexWhereメソッドは、指定した条件に一致する最後の要素のインデックスを返します。

List<String> fruits = ["apple", "banana", "orange", "banana"];
int index = fruits.lastIndexOf("banana");
expect(index, equals(3));

lastIndexWhereメソッドは、条件に一致する最後の要素の位置を見つけるのに便利です。

singleWhere

singleWhereメソッドは、リストの中で指定した条件に一致する唯一の要素を返します。一致する要素が複数ある場合や存在しない場合は例外が発生します。

List<int> numbers = [1, 2, 3, 4, 5];
int number = numbers.singleWhere((num) => num < 2);
expect(number, equals(1));

expect(
  () => numbers.singleWhere((num) => num < 5),
  throwsA(isA<StateError>()),
);

expect({1, 2, 3}.singleWhere((num) => num < 2), 1);

singleWhereメソッドは、条件に一致する唯一の要素を取得するのに便利ですが、一致する要素がなかったり、複数ある場合は例外が発生するため注意が必要です。そのときは、firstWhereに見つからなかった時を指定するか、whereを使いましょう。

要素の変換

Listの要素を別の形に変換するために、asMap、toList、toSet、join、expand、followedBy、mapなどのメソッドを使用します。これらのメソッドを使うことで、データを異なる形式に変換できます。

asMap

asMapメソッドは、ListをMapに変換します。キーはインデックス、値はリストの要素です。

List<String> fruits = ["apple", "banana", "orange"];
Map<int, String> fruitsMap = fruits.asMap();
expect(fruitsMap, equals({0: 'apple', 1: 'banana', 2: 'orange'}));

asMapメソッドは、リストをキーと値のペアに変換するのに便利です。

toList

toListメソッドは、IterableをListに変換します。

Set<String> fruitsSet = {"apple", "banana", "orange"};
List<String> fruitsList = fruitsSet.toList();
expect(fruitsList, equals(["apple", "banana", "orange"]));

toListメソッドは、異なるコレクションからListに変換する際に使用されます。
Setは順番がないため、順番通りになっていることを期待するのは良くない。

toSet

toSetメソッドは、IterableをSetに変換します。重複する要素は1つにまとめられます。

List<String> fruitsList = ["apple", "banana", "orange", "apple"];
Set<String> fruitsSet = fruitsList.toSet();
expect(fruitsSet, equals({"apple", "banana", "orange"}));

toSetメソッドは、重複を排除したユニークな要素の集合を作成するのに便利です。

join

joinメソッドは、リストの要素を指定した区切り文字で連結した文字列を返します。

List<String> fruits = ["apple", "banana", "orange"];
String joinedResult = fruits.join(", ");
expect(joinedResult, equals("apple, banana, orange"));

expand

expandメソッドは、リストの要素を0個以上の要素に展開します。

final listList = [
  [1, 2, 3],
  [4, 5, 6]
];

expect(listList.expand((e) => e), [1, 2, 3, 4, 5, 6]);
final listList2 = [
  [
    [1, 2, 3],
    [4, 5, 6]
  ],
  [
    [7, 8, 9]
  ]
];
expect(
  listList2.expand((e) => e).expand((e) => e),
  [1, 2, 3, 4, 5, 6, 7, 8, 9],
);

List<int> numbers = [1, 2, 3];
List<int> expandedResult = numbers.expand((e) => [e, e * e]).toList();
expect(expandedResult, equals([1, 1, 2, 4, 3, 9]));

expandメソッドは、リストの要素を展開して新しいリストを作成するのに便利です。Python の flatten みたいに使えます。

followedBy

followedByメソッドは、リストの要素に指定したIterableの要素を連結します。

List<String> fruits = ["apple", "banana"];
List<String> moreFruits = ["orange", "grape"];
List<String> followedByResult = fruits.followedBy(moreFruits).toList();
expect(followedByResult, equals(["apple", "banana", "orange", "grape"]));

followedByメソッドは、複数のコレクションを連結するのに適しています。

map

mapメソッドは、リストの各要素に対して指定した処理を実行し、新しいIterableを返します。

List<int> numbers = [1, 2, 3];
List<int> result = numbers.map((num) => num * 2).toList();
expect(result, equals([2, 4, 6]));

mapメソッドは、リストの各要素を変換するのに非常に便利です。

reduce

reduceメソッドは、リスト内の全ての要素を特定の演算で畳み込み、一つの値に集約します。

List<int> numbers = [1, 2, 3];
int sum = numbers.reduce((e1, e2) => e1 + e2);
expect(sum, equals(6));

expect([1].reduce(((e1, e2) => e1 + e2)), 1);

// reduceは要素なしで実施すると、例外が発生する
expect(() => [].reduce((e1, e2) => e1 + e2), throwsA(isA<StateError>()));

reduceメソッドは、リストの全ての要素を集約するのに便利ですが、空のリストでは例外が発生するため注意が必要です。

fold

foldメソッドは、リストの全ての要素を指定した初期値から順に処理して、一つの値に集約します。

List<int> numbers = [1, 2, 3];
int sum = numbers.fold(0, (prev, element) => prev + element);
expect(sum, equals(6));

expect(<int>[].fold(0, (p, e) => p + e), 0);

foldメソッドは初期値を設定できるため、空のリストでも例外が発生せずに処理を行うことができます。

foldで集計

foldメソッドを使用して、リスト内の要素を集計し、Mapに格納することができます。

final map = [1, 2, 3, 4, 1, 2].fold(
    <int, int>{},
    (result, e) => result
      ..update(
        e,
        (e) => 1 + result[e]!,
        ifAbsent: () => 1,
      ));
expect(map[1], 2);
expect(map[3], 1);

このように、foldメソッドを使ってリスト内の要素を集計し、特定のキーに対する出現回数をMapとして取得できます。updateの前に..を付けることで、updateの戻り値ではなく、result自身を戻して、次の演算子に使うようにしています。

要素の並び替え

Listの要素を並び替えるためには、sortshuffleメソッドを使用します。sortメソッドはリストの要素を指定された順序で並べ替え、shuffleメソッドはランダムに並べ替えます。

例えば、以下のように要素をソートします。

sort

sortメソッドは、リストの要素を昇順や指定した基準で並べ替えるために使用します。

    List<int> numbers = [5, 3, 8, 1];
    numbers.sort();
    expect(numbers, equals([1, 3, 5, 8]));

    // 順番の入れ替え
    numbers.sort((e1, e2) => e1 - e2);
    expect(numbers, equals([1, 3, 5, 8]));
    numbers.sort((e1, e2) => e2 - e1);
    expect(numbers, equals([8, 5, 3, 1]));

    // aの数をソートする
    int sortByCountA(String a, String b) =>
        a.split('').where((e) => e == 'a').length -
        b.split('').where((e) => e == 'a').length;

    List<String> fruits = ["apple", "banana", "cherry"];
    fruits.sort(sortByCountA);
    expect(fruits, equals(["cherry", "apple", "banana"]));

sortメソッドは、デフォルトでは昇順にソートされますが、カスタムの比較関数を指定することで、特定の条件に基づいたソートも可能です。

shuffle

shuffleメソッドは、リストの要素をランダムに並べ替えるために使用します。

List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
numbers.shuffle();
expect(numbers.length, equals(9));
expect(numbers.toSet(), equals({1, 2, 3, 4, 5, 6, 7, 8, 9}));
expect(numbers, isNot([1, 2, 3, 4, 5, 6, 7, 8, 9])); // すごくたまに一致して、失敗する

shuffleメソッドは、ゲームやランダムな要素の表示が必要な場合に非常に有用です。

部分リストの取得

Listから部分リストを取得するためには、sublistgetRangeskipskipWhiletaketakeWhileなどのメソッドを使用します。これらのメソッドは、特定の範囲や条件に基づいてリストの一部を取得するために使用されます。

sublist

sublistメソッドは、指定した範囲の部分リストを返します。

List<String> fruits = ["apple", "banana", "orange", "grape"];
List<String> sub = fruits.sublist(1, 3);
expect(sub, equals(["banana", "orange"]));

sublistメソッドは、開始位置と終了位置を指定して部分リストを取得するのに便利です。

getRange

getRangeメソッドは、指定した範囲のIterableを返します。範囲の終端は含まれません。

List<String> fruits = ["apple", "banana", "orange", "grape"];
Iterable<String> range = fruits.getRange(1, 3);
expect(range.toList(), equals(["banana", "orange"]));

getRangeメソッドは、範囲指定で部分リストを取得する際に使用します。

skip

skipメソッドは、指定した数の要素をスキップし、残りの要素を返します。

List<String> fruits = ["apple", "banana", "orange", "grape"];
Iterable<String> skipped = fruits.skip(2);
expect(skipped.toList(), equals(["orange", "grape"]));

skipメソッドは、先頭から指定した数の要素を飛ばして残りの要素を取得するのに便利です。

skipWhile

skipWhileメソッドは、指定した条件に一致する要素をスキップし、条件に一致しなくなった時点以降の要素を返します。

List<int> numbers = [1, 2, 3, 4, 5];
Iterable<int> result = numbers.skipWhile((num) => num < 3);
expect(result.toList(), equals([3, 4, 5]));

skipWhileメソッドは、条件に基づいて要素をスキップするのに便利です。

take

takeメソッドは、指定した数の要素を返します。

List<String> fruits = ["apple", "banana", "orange", "grape"];
Iterable<String> taken = fruits.take(2);
expect(taken.toList(), equals(["apple", "banana"]));

takeメソッドは、先頭から指定した数の要素を取得するのに便利です。

takeWhile

takeWhileメソッドは、指定した条件に一致する要素を先頭から順に返し、条件に一致しなくなった時点で終了します。

List<int> numbers = [1, 2, 3, 4, 5];
Iterable<int> result = numbers.takeWhile((num) => num < 4);
print(result.toList()); // [1, 2, 3]

takeWhileメソッドは、条件に基づいて要素を取得するのに便利です。

Setの操作

要素の追加

Setには、要素を追加するためのaddaddAllメソッドがあります。addメソッドは単一の要素を追加し、addAllメソッドは他のコレクションから要素を追加します。Setは重複を許さないため、重複する要素は追加されません。

例えば、以下のように要素を追加します。

Set<String> fruits = {'apple', 'banana'};
fruits.add('orange');
expect(fruits, equals({'apple', 'banana', 'orange'}));

Set<String> moreFruits = {'grape', 'lemon'};
fruits.addAll(moreFruits);
expect(fruits, equals({'apple', 'banana', 'orange', 'grape', 'lemon'}));

SetのaddaddAllメソッドを使用することで、重複のないユニークな要素を簡単に追加できます。

要素の削除

Setの要素を削除するためには、removeremoveAllremoveWhere、およびclearメソッドがあります。removeメソッドは指定した要素を、removeAllメソッドは他のコレクションの要素全てを削除します。removeWhereメソッドは条件に一致する要素を削除し、clearメソッドはSet内の全ての要素を削除します。

例えば、以下のように要素を削除します。

Set<String> fruits = {'apple', 'banana', 'orange', 'grape'};
fruits.remove('banana');
expect(fruits, equals({'apple', 'orange', 'grape'}));

Set<String> toRemove = {'orange', 'grape'};
fruits.removeAll(toRemove);
expect(fruits, equals({'apple'}));

fruits.addAll({'lemon', 'melon'});
fruits.removeWhere((fruit) => fruit.startsWith('l'));
expect(fruits, equals({'apple', 'melon'}));

fruits.clear();
expect(fruits, equals(<String>{}));

これにより、必要に応じてSetから要素を柔軟に削除することができます。

要素の存在確認

Setの要素の存在確認は、containscontainsAllメソッドを使用します。containsメソッドは単一の要素が存在するかを、containsAllメソッドは指定したコレクションの全ての要素が存在するかを確認します。

例えば、以下のように要素の存在を確認します。

Set<String> fruits = {'apple', 'banana', 'orange'};
expect(fruits.contains('banana'), equals(true));
expect(fruits.contains('grape'), equals(false));

Set<String> subset = {'apple', 'orange'};
expect(fruits.containsAll(subset), equals(true));

要素の存在を確認することで、特定の操作を行う前にデータの状態をチェックすることができます。

要素の取得

Setの特定の要素を取得するためには、elementAtメソッドを使用します。Setは順序を保証しないため、インデックスを使用する場合は注意が必要です。

例えば、以下のように要素を取得します。

Set<String> fruits = {'apple', 'banana', 'orange'};
String fruit = fruits.elementAt(1);
expect(fruits.contains(fruit), equals(true)); // 順序は保証されないため、存在確認で検証
expect(fruit, 'banana'); // 順序は保証されないため、当てにならない

elementAtメソッドはインデックスを指定して要素を取得しますが、Setの順序が保証されないため、特定の要素の取得には適していません。

その他の便利な関数

Setには、要素の集合を操作するための便利な関数として、unionintersectiondifferenceがあります。これらのメソッドは、2つのSetの和集合、積集合、差集合を求めるために使用します。

例えば、以下のようにSetの集合操作を行います。

Set<String> setA = {'apple', 'banana', 'orange'};
Set<String> setB = {'banana', 'grape', 'lemon'};

Set<String> unionSet = setA.union(setB);
expect(unionSet, equals({'apple', 'banana', 'orange', 'grape', 'lemon'}));

Set<String> intersectionSet = setA.intersection(setB);
expect(intersectionSet, equals({'banana'}));

Set<String> differenceSet = setA.difference(setB);
expect(differenceSet, equals({'apple', 'orange'}));

これにより、異なるSet間の要素の関係を簡単に操作することができます。

Mapの操作

要素の追加

Mapには、要素を追加するための様々なメソッドがあります。putIfAbsentは指定したキーが存在しない場合に値を追加し、addEntriesはMapEntryのリストを追加し、addAllは別のMapの全ての要素を追加します。

例えば、以下のように要素を追加します。

putIfAbsent

putIfAbsentメソッドは、指定したキーがMapに存在しない場合に値を追加します。

Map<String, int> scores = {"Math": 90, "English": 85};
scores.putIfAbsent("Science", () => 78);
scores.putIfAbsent("Math", () => 100); // 存在するので無効
expect(scores, equals({"Math": 90, "English": 85, "Science": 78}));

このメソッドは、キーが存在しない場合にのみ値を追加するため、既存のデータを保護しつつ新しいデータを追加するのに適しています。

addEntries

addEntriesメソッドは、MapEntryのリストをMapに追加します。

Map<String, int> scores = {"Math": 90, "English": 85};
scores.addEntries([MapEntry("Science", 78), MapEntry("History", 88)]);
expect(scores,
  equals({"Math": 90, "English": 85, "Science": 78, "History": 88}));

scores.addEntries([MapEntry("History", 100)]);
expect(scores,
  equals({"Math": 90, "English": 85, "Science": 78, "History": 100}));

このメソッドは、一度に複数のキーと値のペアを追加するのに便利です。

addAll

addAllメソッドは、別のMapの全ての要素を追加します。

Map<String, int> scores = {"Math": 90};
Map<String, int> additionalScores = {"English": 85, "Science": 78};
scores.addAll(additionalScores);
expect(scores, equals({"Math": 90, "English": 85, "Science": 78}));

このメソッドは、複数のMapを統合するのに使用されます。

要素の削除

Mapから要素を削除するためには、removeremoveWhere、およびclearメソッドを使用します。removeメソッドは指定したキーの要素を削除し、removeWhereメソッドは条件に一致する要素を削除し、clearメソッドはMap内の全ての要素を削除します。

Map<String, int> scores = {"Math": 90, "English": 85, "Science": 78};
scores.remove("English");
expect(scores, equals({"Math": 90, "Science": 78}));

scores.removeWhere((key, value) => value < 80);
expect(scores, equals({"Math": 90}));

scores.clear();
expect(scores.length, 0);

このメソッドは、Mapを初期化するのに便利です。

要素の存在確認

Mapの要素の存在確認は、containsKeycontainsValueメソッドを使用します。containsKeyメソッドは特定のキーが存在するかを確認し、containsValueメソッドは特定の値が存在するかを確認します。

Map<String, int> scores = {"Math": 90, "English": 85};
expect(scores.containsKey("Math"), equals(true));
expect(scores.containsKey("Science"), equals(false));

expect(scores.containsValue(90), equals(true));
expect(scores.containsValue(100), equals(false));

要素の変更

Mapの要素を変更するためには、updateupdateAllメソッドを使用します。updateメソッドは指定したキーの値を更新し、updateAllメソッドは全ての要素の値を更新します。

Map<String, int> scores = {'Math': 90, 'English': 85};
scores.update('Math', (value) => value + 5);
expect(scores, equals({'Math': 95, 'English': 85}));

scores.updateAll((key, value) => value + 5);
expect(scores, equals({'Math': 100, 'English': 90}));

要素の確認

Mapの全ての要素を確認するためには、forEachメソッドを使用します。forEachメソッドは、全てのキーと値のペアに対して指定した処理を実行します。

Map<String, int> scores = {'Math': 90, 'English': 85};
scores.forEach((key, value) {
    expect(key, anyOf(equals('Math'), equals('English')));
    expect(value, anyOf(equals(90), equals(85)));
});

このメソッドは、全ての要素を順番に処理するのに便利です。

要素の変換

Mapの要素を別の形式に変換するためには、mapメソッドを使用します。mapメソッドは、指定した関数に基づいて新しいMapを生成します。

Map<String, int> scores = {'Math': 90, 'English': 85};
Map<String, String> result =
  scores.map((key, value) => MapEntry(key, 'Score: $value'));
expect(result, equals({'Math': 'Score: 90', 'English': 'Score: 85'}));

このメソッドは、Mapの要素を変換するのに非常に便利です。

SetやListからMapへ変換

Dartでは、SetやListをMapに変換するための便利な方法がいくつかあります。本記事では、Map.fromIterable() や for ループを用いた変換方法、そして文字列の長さに基づいた変換例を紹介します。

Map.fromIterable() を使用した変換

DartのMap.fromIterable()メソッドは、Iterable(ListやSetなど)からキーと値を生成してMapを作成する方法です。ただ実際に書くと、「forループを使った変換」をlintから進められます(個人的にもそちらの方が直感的)。以下の例では、List内の要素をキーに、その要素を文字列に変換したものを値にしてMapを生成しています。

test('fromIteration', () {
  final list = [1, 2, 3];

  final map = Map.fromIterable(list, key: (e) => e, value: (e) => e.toString());
  expect(map[1], '1');
  expect(map[2], '2');
  expect(map[3], '3');
});

このテストでは、数値のListを使い、その要素をキーに、文字列に変換した値を持つMapを作成しています。

forループを使った変換

Dartでは、forループを使っても簡単にListやSetをMapに変換できます。以下の例では、forループを使用してListの要素を文字列に変換したMapを作成しています。

test('for', () {
  final list = [1, 2, 3];

  final map = {for (final key in list) key: key.toString()};
  expect(map[1], '1');
  expect(map[2], '2');
  expect(map[3], '3');
});

この例でも、各数値をキーにして、その数値の文字列バージョンを値に持つMapを生成しています。

文字列の長さに基づく変換

次に、少し高度な例として、Listの要素の文字列の長さに基づいて、要素をMapに変換する方法を紹介します。以下のコードでは、文字列の長さをキーに、同じ長さの文字列をSetにしてMapに格納しています。

test('文字列の長さ', () {
  final list = <String>['a', 'b', 'cc', 'dd', 'eee'];
  final map = {
    for (final key in list.map((e) => e.length).toSet())
      key: list.where((e) => e.length == key).toSet()
  };
  expect(map[1], {'a', 'b'});
  expect(map[2], {'cc', 'dd'});
  expect(map[3], {'eee'});
});

このコードでは、文字列の長さをキーにして、その長さに一致する文字列をSetにまとめています。例として簡単になるように文字数にしてますが、実際にはList内の特定の項目に基づいてグループ分けするなどが可能になります。懸念事項としてkey毎にwhereを使っているため、whereを使う回数が多くなっています。データが多い場合、foldなどで実行回数を減らす方が良いかもしれません。

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

==の動作と等価チェック

Dartのコレクション(Set、List、Map)において、==演算子での比較と等価性チェックを行います。パッケージcollectionがdart.dev作で標準っぽいので、そちらを使います。

Set

Setの等価性比較において、==演算子とSetEqualityの違いを確認します。

  • == 演算子: インスタンスの等価性を比較
  • SetEquality().equals(): 要素の等価性を比較
import 'package:collection/collection.dart';
final set1 = {'1'};
final set2 = {'1'};
expect(set1 == set2, false);

expect(const SetEquality().equals(set1, set2), true);

List

Listの等価性比較において、==演算子とListEqualityの違いを確認します。

  • == 演算子: インスタンスの等価性を比較
  • ListEquality().equals(): 要素の等価性を比較
import 'package:collection/collection.dart';
final list1 = ['1'];
final list2 = ['1'];
expect(list1 == list2, false);

expect(const ListEquality().equals(list1, list2), true);

Map

Mapの等価性比較において、==演算子とMapEqualityの違いを確認します。

  • == 演算子: インスタンスの等価性を比較
  • MapEquality().equals(): エントリの等価性を比較
import 'package:collection/collection.dart';
final map1 = {'1': '2'};
final map2 = {'1': '2'};
expect(map1 == map2, false);
expect(map1.entries == map2.entries, false);

expect(const MapEquality().equals(map1, map2), true);

新しいコレクションの作成

Riverpodでは、インスタンスが別のものに入れ替わったことを確認して、データ変更を検出します。そのため、インスタンス自体が変わらないと、データ更新が伝わらず、画面の更新も行われません。これは初心者がRiverpodを初めて使用する際に迷いやすいポイントです。

通常のリストやセット、マップのメソッドを使用した場合、そのコレクションのインスタンスは変更されません。結果として、Riverpod側からはデータが更新されたことが認識されず、意図した通りに動作しないことがあります。そこで、Riverpod用に新しいインスタンスを作成する方法を解説し、データ更新を正しく伝える手法を紹介します。このセクションでは、リスト、セット、マップの新しいコレクションのインスタンスを作成する方法について詳しく説明します。新しいインスタンスをRiverpodの更新の値として使えば、正しく画面更新がされます。

Spread演算子とCascade記法

初心者には馴染みが薄いですが、今回重要になりますので解説します

  • Spread演算子:
    Spread演算子は、リスト、セット、マップなどのコレクションを展開して、新しいコレクションを作成するために使用されます。...を使って元のコレクションの要素を展開します。

  • Cascade記法:
    Cascade記法は、同じオブジェクトに対して連続してメソッドを呼び出す際に使用されます。..を使って、オブジェクトを変更しながらそのオブジェクトを返します

コレクションの複製

既存のコレクションを複製するには、スプレッド演算子(…)を使用します。これにより、新しいインスタンスが作成されます。

// Listの複製
final list = [1, 2];
final anotherList = [...list];

expect(identical(list, anotherList), isFalse);
expect(list, anotherList);

// Setの複製
final set = {1, 2};
final anotherSet = {...set};

expect(identical(set, anotherSet), isFalse);
expect(set, equals(anotherSet));

// Mapの複製
final map = {1: 'one', 2: 'two'};
final anotherMap = {...map};

expect(identical(map, anotherMap), isFalse);
expect(map, equals(anotherMap));

新規コレクションへの要素追加

既存のコレクションに要素を追加しつつ新しいインスタンスを作成するには、スプレッド演算子と追加要素を組み合わせます。

// Listに要素追加
final list = [1, 2];
final anotherList = [...list, 3];

expect(identical(list, anotherList), isFalse);
expect(list.length, 2);
expect(anotherList.length, 3);

// Setに要素追加
final set = {1, 2};
final anotherSet = {...set, 3};

expect(identical(set, anotherSet), isFalse);
expect(set.length, 2);
expect(anotherSet.length, 3);

// Mapに要素追加
final map = {1: 'one', 2: 'two'};
final anotherMap = {...map, 3: 'three'};

expect(identical(map, anotherMap), isFalse);
expect(map.length, 2);
expect(anotherMap.length, 3);

新規コレクションで要素削除

既存のコレクションから要素を削除しつつ新しいインスタンスを作成するには、スプレッド演算子とカスケード演算子(..)を組み合わせます。

// Listから要素削除
final list = [1, 2];
final anotherList = [...list..remove(1)];

expect(identical(list, anotherList), isFalse);
expect(list.length, 1);
expect(anotherList.length, 1);

// Setから要素削除
final set = {1, 2};
final anotherSet = {...set..remove(1)};

expect(identical(set, anotherSet), isFalse);
expect(set.length, 1);
expect(anotherSet.length, 1);

// Mapから要素削除
final map = {1: 'one', 2: 'two'};
final anotherMap = {...map..remove(1)};

expect(identical(map, anotherMap), isFalse);
expect(map.length, 1);
expect(anotherMap.length, 1);

インデックスで削除して、新規List

既存のListから指定したインデックスの要素を削除し、新しいインスタンスを作成するには、getRangeメソッドとスプレッド演算子を組み合わせます。
この方法を使うことで、元のリストを変更せずに新しいリストを作成し、指定したインデックスの要素を取り除くことができます。これにより、元のデータを保護しつつ新しいインスタンスを作成することができます。

// 指定したインデックスで要素を削除して新しいListを作成する関数
List<int> removeByIndex(List<int> list, int index) {
  return [
    ...list.getRange(0, index),
    ...list.getRange(index + 1, list.length)
  ];
}

// Listから要素削除
final list = [0, 1, 2, 3];
expect(removeByIndex(list, 0), equals([1, 2, 3]));
expect(removeByIndex(list, 1), equals([0, 2, 3]));
expect(removeByIndex(list, 2), equals([0, 1, 3]));
expect(removeByIndex(list, 3), equals([0, 1, 2]));

expect(removeByIndex([1], 0), equals([]));

このように、Dartではスプレッド演算子やカスケード演算子を使用して、新しいインスタンスを作成しながらコレクションを操作することができます。これにより、Riverpodなどのステート管理ライブラリと組み合わせて効果的にデータ更新を検知することができます。

Q&A

Q1: Set、List、Mapの違いは何ですか?

A1: Setは重複を許さないユニークな要素の集合で順序を保持しません。Listは順序付けられた重複要素を持つコレクションで、インデックスでアクセスできます。Mapはキーと値のペアを持ち、キーが一意である必要がありますが、値の重複を許容します。

Q2: Listの要素を追加する方法は?

A2: ListにはaddaddAllメソッドがあります。addメソッドは単一の要素を末尾に追加し、addAllメソッドは別のListの全ての要素を末尾に追加します。これにより、Listに柔軟に要素を追加できます。

Q3: Mapの要素を削除する方法は?

A3: MapにはremoveremoveWhereclearメソッドがあります。removeは指定したキーの要素を削除し、removeWhereは条件に一致する要素を削除、clearは全ての要素を削除します。これにより、Map内の要素を効果的に管理できます。

まとめ

この記事を通して、Set、List、Mapの違いと共通点について勉強しました。Setは重複を許さず、順序を保持しないが、ユニークな要素を管理するのに便利です。Listは順序を持ち、重複を許容するため、順序が重要なデータの管理に適しています。Mapはキーと値のペアを管理し、キーが一意であることを求めますが、値の重複を許容します。各データ構造の操作方法も学び、要素の追加、削除、存在確認、繰り返し処理ができるようになりました。これにより、Dartで効果的なデータ管理が可能となりました。

参考

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

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Set,List,Mapの違い/共通点', () {
    test('重複要素の扱い', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'apple'};
      expect(fruitsSet, equals({'apple', 'banana'}));
      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'apple'];
      expect(fruitsList, equals(['apple', 'banana', 'apple']));
      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'a': 'apricot'
      };
      expect(fruitsMap, equals({'a': 'apricot', 'b': 'banana'}));
    });
    test('要素のアクセス方法', () {
      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'cherry'];
      expect(fruitsList[1], equals('banana'));

      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
      expect(fruitsSet.contains('banana'), equals(true));

      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'c': 'cherry'
      };
      expect(fruitsMap['b'], equals('banana'));
    });

    test('要素の追加', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana'};
      fruitsSet.add('cherry');
      expect(fruitsSet, equals({'apple', 'banana', 'cherry'}));

      // Listの場合
      List<String> fruitsList = ['apple', 'banana'];
      fruitsList.add('cherry');
      expect(fruitsList, equals(['apple', 'banana', 'cherry']));

      // Mapの場合
      Map<String, String> fruitsMap = {'a': 'apple', 'b': 'banana'};
      fruitsMap.putIfAbsent('c', () => 'cherry');
      expect(fruitsMap, equals({'a': 'apple', 'b': 'banana', 'c': 'cherry'}));
    });
    test('要素の削除', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
      fruitsSet.remove('banana');
      expect(fruitsSet, equals({'apple', 'cherry'}));

      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'cherry'];
      fruitsList.remove('banana');
      expect(fruitsList, equals(['apple', 'cherry']));
      fruitsList.removeAt(1);
      expect(fruitsList, equals(['apple']));

      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'c': 'cherry'
      };
      fruitsMap.remove('b');
      expect(fruitsMap, equals({'a': 'apple', 'c': 'cherry'}));
    });
    test('要素の存在確認', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
      expect(fruitsSet.contains('banana'), equals(true));
      expect(fruitsSet.length, 3);

      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'cherry'];
      expect(fruitsList.contains('banana'), equals(true));
      expect(fruitsList.length, 3);

      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'c': 'cherry'
      };
      expect(fruitsMap.containsKey('b'), equals(true));
      expect(fruitsMap.containsValue('banana'), equals(true));
      expect(fruitsMap.length, 3);
    });
    test('要素の繰り返し処理 for', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
      for (var fruit in fruitsSet) {
        expect(fruitsSet.contains(fruit), equals(true));
      }

      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'cherry'];
      for (var fruit in fruitsList) {
        expect(fruitsList.contains(fruit), equals(true));
      }

      for (int index = 0; index < fruitsList.length; index++) {
        var fruit = fruitsList[index];
        expect(fruitsList.contains(fruit), equals(true));
      }

      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'c': 'cherry'
      };
      for (var entry in fruitsMap.entries) {
        expect(fruitsMap.containsKey(entry.key), equals(true));
        expect(fruitsMap[entry.key], equals(entry.value));
      }
      for (var key in fruitsMap.keys) {
        expect(fruitsMap.containsKey(key), equals(true));
      }
      for (var value in fruitsMap.values) {
        expect(fruitsMap.containsValue(value), equals(true));
      }
    });
    test('要素の繰り返し処理: forEach', () {
      // Setの場合
      Set<String> fruitsSet = {'apple', 'banana', 'cherry'};
      fruitsSet
          .forEach((fruit) => expect(fruitsSet.contains(fruit), equals(true)));

      // Listの場合
      List<String> fruitsList = ['apple', 'banana', 'cherry'];
      fruitsList
          .forEach((fruit) => expect(fruitsList.contains(fruit), equals(true)));

      // Mapの場合
      Map<String, String> fruitsMap = {
        'a': 'apple',
        'b': 'banana',
        'c': 'cherry'
      };
      fruitsMap.forEach((key, value) {
        expect(fruitsMap.containsKey(key), equals(true));
        expect(fruitsMap[key], equals(value));
      });
      fruitsMap.keys
          .forEach((key) => expect(fruitsMap.containsKey(key), equals(true)));
      fruitsMap.values.forEach(
          (value) => expect(fruitsMap.containsValue(value), equals(true)));

      // 以下は許される書き方
      fruitsMap.entries.forEach(print);
    });

    test('空のコレクションの作成と確認', () {
      // Listの空のコレクションの作成とテスト
      final emptyList = <int>[];
      expect(emptyList.isEmpty, true);
      expect(emptyList.isNotEmpty, false);

      // Setの空のコレクションの作成とテスト
      final emptySet = <int>{};
      expect(emptySet.isEmpty, true);
      expect(emptySet.isNotEmpty, false);

      // Mapの空のコレクションの作成とテスト
      final emptyMap = <int, int>{};
      expect(emptyMap.isEmpty, true);
      expect(emptyMap.isNotEmpty, false);

      expect([], isA<List>());
      expect({}, isA<Map>());
      expect({}, isNot(isA<Set>()));
    });
  });
  group('Listの操作', () {
    test('Listの作成', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      List<int> numbers1 = [1, 2, 3, 4, 5];
      var numbers2 = [1, 2, 3, 4, 5];
      var numbers3 = <int>[1, 2, 3, 4, 5];
      final numbers4 = <int>[1, 2, 3, 4, 5];

      final dynamicList = [1, '2', 3, '4', '5'];
    });

    test('Listの連番の作成', () {
      final list = List.generate(3, (index) => index * 2);
      expect(list, [0, 2, 4]);

      expect(Iterable.generate(3), [0, 1, 2]);
      expect(Iterable.generate(3, (e) => e * 2), [0, 2, 4]);
    });

    test('要素のアクセス', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      String firstFruit = fruits[0]; // 'apple'
      fruits[1] = 'grape'; // 'banana'を'grape'に変更
      expect(fruits, equals(['apple', 'grape', 'orange']));
    });
    test('要素の追加', () {
      List<String> fruits = ["apple", "banana"];
      fruits.add("orange");
      expect(fruits, equals(["apple", "banana", "orange"]));

      List<String> moreFruits = ["grape", "melon"];
      fruits.addAll(moreFruits);
      expect(fruits, equals(["apple", "banana", "orange", "grape", "melon"]));

      fruits.insert(1, "orange");
      expect(fruits,
          equals(["apple", "orange", "banana", "orange", "grape", "melon"]));

      fruits.insertAll(2, ["grape", "melon"]);
      expect(fruits, [
        "apple",
        "orange",
        "grape",
        "melon",
        "banana",
        "orange",
        "grape",
        "melon"
      ]);
    });
    test('要素の存在確認', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      bool hasApple = fruits.contains('apple'); // true
      expect(hasApple, equals(true));

      bool hasPear = fruits.contains('pear'); // false
      expect(hasPear, equals(false));

      List<int> numbers = [1, 5, 10];
      bool anyGreaterThanFive = numbers.any((num) => num > 5);
      expect(anyGreaterThanFive, equals(true));

      bool allGreaterThanFive = numbers.every((num) => num > 5);
      expect(allGreaterThanFive, equals(false));
    });
    test('要素の変更', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      fruits.setAll(1, ['grape', 'melon']);
      expect(fruits, equals(['apple', 'grape', 'melon']));

      fruits.setRange(0, 2, ['kiwi', 'pineapple']);
      expect(fruits, equals(['kiwi', 'pineapple', 'melon']));

      fruits.fillRange(1, 3, 'lemon');
      expect(fruits, equals(['kiwi', 'lemon', 'lemon']));

      fruits.replaceRange(0, 2, ['strawberry']);
      expect(fruits, equals(['strawberry', 'lemon']));
    });
    test('要素の削除', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
      fruits.remove('banana');
      expect(fruits, equals(['apple', 'orange', 'grape']));

      fruits.removeAt(1);
      expect(fruits, equals(['apple', 'grape']));

      fruits.removeLast();
      expect(fruits, equals(['apple']));
      // fruits.removeFirst() はない

      fruits.addAll(['melon', 'lemon', 'pear']);
      expect(fruits, equals(['apple', 'melon', 'lemon', 'pear']));

      // インデックス1 から削除する。インデックス3 は削除しない
      fruits.removeRange(1, 3);
      expect(fruits, equals(['apple', 'pear']));

      // 削除する条件を指定
      fruits.removeWhere((fruit) => fruit.startsWith('p'));
      expect(fruits, equals(['apple']));

      fruits = ['apple', 'pear'];
      // 削除しない条件を指定
      fruits.retainWhere((fruit) => fruit.startsWith('p'));
      expect(fruits, ['pear']);

      fruits.clear();
      expect(fruits, equals([]));
    });
    test('forEach', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      fruits.forEach((fruit) => expect(fruits.contains(fruit), equals(true)));
    });
    test('Where', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      final numbersWhere = numbers.where((num) => num < 4);
      expect(numbersWhere.length, 3);

      //Setも同様
      expect({1, 2, 3, 4, 5}.where((num) => num < 4).length, 3);
    });
    test('elementAt', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      String fruit = fruits.elementAt(1);
      expect(fruit, equals('banana'));
    });
    test('firstWhere', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      int number = numbers.firstWhere((num) => num > 3);
      expect(number, equals(4));

      // 見つからない場合は例外
      expect(() => [].firstWhere((num) => num > 3), throwsA(isA<StateError>()));

      expect([].firstWhere((num) => num > 3, orElse: () => -1), -1);
    });
    test('lastWhere', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      int number = numbers.lastWhere((num) => num < 4);
      expect(number, equals(3));
    });
    test('singleWhere', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      int number = numbers.singleWhere((num) => num < 2);
      expect(number, equals(1));
      expect(
        () => numbers.singleWhere((num) => num < 5),
        throwsA(isA<StateError>()),
      );

      expect({1, 2, 3}.singleWhere((num) => num < 2), 1);
    });

    test('indexOf', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      int index = fruits.indexOf('banana');
      expect(index, equals(1));

      // 見つからない場合、-1を返す
      expect(fruits.indexOf('pear'), -1);
    });

    test('indexWhere', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      int index = fruits.indexWhere((fruit) => fruit.startsWith('o'));
      expect(index, equals(2));
    });
    test('lastIndexOf', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'banana'];
      int index = fruits.lastIndexOf('banana');
      expect(index, equals(3));
    });
    test('asMap', () {
      List<String> fruits = ['apple', 'banana', 'orange'];
      Map<int, String> fruitsMap = fruits.asMap();
      expect(fruitsMap, equals({0: 'apple', 1: 'banana', 2: 'orange'}));
    });
    test('toList', () {
      Set<String> fruitsSet = {'apple', 'banana', 'orange'};
      List<String> fruitsList = fruitsSet.toList();
      expect(fruitsList, equals(['apple', 'banana', 'orange']));
    });
    test('toSet', () {
      List<String> fruitsList = ['apple', 'banana', 'orange', 'apple'];
      Set<String> fruitsSet = fruitsList.toSet();
      expect(fruitsSet, equals({'apple', 'banana', 'orange'}));
    });

    test('join', () {
      List<String> fruits = ["apple", "banana", "orange"];
      String joinedResult = fruits.join(", ");
      expect(joinedResult, equals("apple, banana, orange"));
    });


    test('expand', () {
      final listList = [
        [1, 2, 3],
        [4, 5, 6]
      ];
      expect(listList.expand((e) => e), [1, 2, 3, 4, 5, 6]);

      List<int> numbers = [1, 2, 3];
      List<int> expandedResult = numbers.expand((e) => [e, e * e]).toList();
      expect(expandedResult, equals([1, 1, 2, 4, 3, 9]));
    });

    test('followedBy', () {
      List<String> fruits = ['apple', 'banana'];
      List<String> moreFruits = ['orange', 'grape'];
      List<String> followedByResult = fruits.followedBy(moreFruits).toList();
      expect(followedByResult, equals(['apple', 'banana', 'orange', 'grape']));
    });

    test('sort', () {
      List<int> numbers = [5, 3, 8, 1];
      numbers.sort();
      expect(numbers, equals([1, 3, 5, 8]));

      // 順番の入れ替え
      numbers.sort((e1, e2) => e1 - e2);
      expect(numbers, equals([1, 3, 5, 8]));
      numbers.sort((e1, e2) => e2 - e1);
      expect(numbers, equals([8, 5, 3, 1]));

      // aの数をソートする
      int sortByCountA(String a, String b) =>
          a.split('').where((e) => e == 'a').length -
          b.split('').where((e) => e == 'a').length;

      List<String> fruits = ['apple', 'banana', 'cherry'];
      fruits.sort(sortByCountA);
      expect(fruits, equals(['cherry', 'apple', 'banana']));
    });
    test('shuffle', () {
      List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
      numbers.shuffle();
      expect(numbers.length, equals(9));
      expect(numbers.toSet(), equals({1, 2, 3, 4, 5, 6, 7, 8, 9}));
      expect(numbers, isNot([1, 2, 3, 4, 5, 6, 7, 8, 9])); // すごくたまに一致して、失敗する
    });
    test('sublist', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
      List<String> sub = fruits.sublist(1, 3);
      expect(sub, equals(['banana', 'orange']));
    });
    test('getRange', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
      Iterable<String> range = fruits.getRange(1, 3);
      expect(range.toList(), equals(['banana', 'orange']));
    });

    test('skip', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
      Iterable<String> skipped = fruits.skip(2);
      expect(skipped.toList(), equals(['orange', 'grape']));
    });
    test('skipWhile', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      Iterable<int> result = numbers.skipWhile((num) => num < 3);
      expect(result.toList(), equals([3, 4, 5]));
    });
    test('take', () {
      List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
      Iterable<String> taken = fruits.take(2);
      expect(taken.toList(), equals(['apple', 'banana']));
    });
    test('takeWhile', () {
      List<int> numbers = [1, 2, 3, 4, 5];
      Iterable<int> result = numbers.takeWhile((num) => num < 4);
      expect(result.toList(), equals([1, 2, 3]));
    });

    test('reduce', () {
      List<int> numbers = [1, 2, 3];
      int sum = numbers.reduce((e1, e2) => e1 + e2);
      expect(sum, equals(6));

      expect([1].reduce(((e1, e2) => e1 + e2)), 1);

      // reduceは要素なしで実施すると、例外が発生する。try-catchもできない
      // expect([].reduce((e1, e2) => e1 + e2), throwsA(isA<StateError>()));
    });

    test('fold', () {
      List<int> numbers = [1, 2, 3];
      int sum = numbers.fold(0, (prev, element) => prev + element);
      expect(sum, equals(6));

      expect(<int>[].fold(0, (p, e) => p + e), 0);
    });
    test('foldで集計', () {
      final map = [1, 2, 3, 4, 1, 2].fold(
          <int, int>{},
          (result, e) => result
            ..update(
              e,
              (e) => 1 + result[e]!,
              ifAbsent: () => 1,
            ));
      expect(map[1], 2);
      expect(map[3], 1);
    });
  });
  group('Setの操作', () {
    test('要素の追加', () {
      Set<String> fruits = {'apple', 'banana'};
      fruits.add('orange');
      expect(fruits, equals({'apple', 'banana', 'orange'}));

      Set<String> moreFruits = {'grape', 'lemon'};
      fruits.addAll(moreFruits);
      expect(fruits, equals({'apple', 'banana', 'orange', 'grape', 'lemon'}));
    });
    test('要素の削除', () {
      Set<String> fruits = {'apple', 'banana', 'orange', 'grape'};
      fruits.remove('banana');
      expect(fruits, equals({'apple', 'orange', 'grape'}));

      Set<String> toRemove = {'orange', 'grape'};
      fruits.removeAll(toRemove);
      expect(fruits, equals({'apple'}));

      fruits.addAll({'lemon', 'melon'});
      fruits.removeWhere((fruit) => fruit.startsWith('l'));
      expect(fruits, equals({'apple', 'melon'}));

      fruits.clear();
      expect(fruits, equals(<String>{}));
    });
    test('要素の存在確認', () {
      Set<String> fruits = {'apple', 'banana', 'orange'};
      expect(fruits.contains('banana'), equals(true));
      expect(fruits.contains('grape'), equals(false));

      Set<String> subset = {'apple', 'orange'};
      expect(fruits.containsAll(subset), equals(true));
    });
    test('要素の取得', () {
      Set<String> fruits = {'apple', 'banana', 'orange'};
      String fruit = fruits.elementAt(1);
      expect(fruits.contains(fruit), equals(true)); // 順序は保証されないため、存在確認で検証
      expect(fruit, 'banana'); // 順序は保証されないため、当てにならない
    });
    test('その他の便利な関数', () {
      Set<String> setA = {'apple', 'banana', 'orange'};
      Set<String> setB = {'banana', 'grape', 'lemon'};

      Set<String> unionSet = setA.union(setB);
      expect(unionSet, equals({'apple', 'banana', 'orange', 'grape', 'lemon'}));

      Set<String> intersectionSet = setA.intersection(setB);
      expect(intersectionSet, equals({'banana'}));

      Set<String> differenceSet = setA.difference(setB);
      expect(differenceSet, equals({'apple', 'orange'}));
    });
  });

  group('Mapの操作', () {
    test('putIfAbsent', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      scores.putIfAbsent('Science', () => 78);
      scores.putIfAbsent('Math', () => 100); // 存在するので無効
      expect(scores, equals({'Math': 90, 'English': 85, 'Science': 78}));
    });
    test('addEntries', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      scores.addEntries([MapEntry('Science', 78), MapEntry('History', 88)]);
      expect(scores,
          equals({'Math': 90, 'English': 85, 'Science': 78, 'History': 88}));

      scores.addEntries([MapEntry('History', 100)]);
      expect(scores,
          equals({'Math': 90, 'English': 85, 'Science': 78, 'History': 100}));
    });
    test('addAll', () {
      Map<String, int> scores = {'Math': 90};
      Map<String, int> additionalScores = {'English': 85, 'Science': 78};
      scores.addAll(additionalScores);
      expect(scores, equals({'Math': 90, 'English': 85, 'Science': 78}));
    });
    test('要素の削除', () {
      Map<String, int> scores = {'Math': 90, 'English': 85, 'Science': 78};
      scores.remove('English');
      expect(scores, equals({'Math': 90, 'Science': 78}));

      scores.removeWhere((key, value) => value < 80);
      expect(scores, equals({'Math': 90}));

      scores.clear();
      expect(scores.length, 0);
    });
    test('要素の存在確認', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      expect(scores.containsKey('Math'), equals(true));
      expect(scores.containsKey('Science'), equals(false));

      expect(scores.containsValue(90), equals(true));
      expect(scores.containsValue(100), equals(false));
    });

    test('要素の変更', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      scores.update('Math', (value) => value + 5);
      expect(scores, equals({'Math': 95, 'English': 85}));

      scores.updateAll((key, value) => value + 5);
      expect(scores, equals({'Math': 100, 'English': 90}));
    });
    test('要素の確認', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      scores.forEach((key, value) {
        expect(key, anyOf(equals('Math'), equals('English')));
        expect(value, anyOf(equals(90), equals(85)));
      });
    });
    test('要素の変換', () {
      Map<String, int> scores = {'Math': 90, 'English': 85};
      Map<String, String> result =
          scores.map((key, value) => MapEntry(key, 'Score: $value'));
      expect(result, equals({'Math': 'Score: 90', 'English': 'Score: 85'}));
    });
    test('メソッド使用せず', () {
      final scores = <String, int>{'Math': 90, 'English': 85};
      scores['Science'] = 70;
      expect(scores.length, 3);
      expect(scores['Math'], 90);
      scores['Math'] = 80;
      expect(scores['Math'], 80);
      expect(scores['noData'], null);
    });
  });

  group('応用例', () {
    test('コレクションの複製', () {
      // Listの複製
      final list = [1, 2];
      final anotherList = [...list];

      expect(identical(list, anotherList), isFalse);
      expect(list, anotherList);

      // Setの複製
      final set = {1, 2};
      final anotherSet = {...set};

      expect(identical(set, anotherSet), isFalse);
      expect(set, equals(anotherSet));

      // Mapの複製
      final map = {1: 'one', 2: 'two'};
      final anotherMap = {...map};

      expect(identical(map, anotherMap), isFalse);
      expect(map, equals(anotherMap));
    });

    test('新規のコレクションで追加', () {
      // Listに要素追加
      final list = [1, 2];
      final anotherList = [...list, 3];

      expect(identical(list, anotherList), isFalse);
      expect(list.length, 2);
      expect(anotherList.length, 3);

      // Setに要素追加
      final set = {1, 2};
      final anotherSet = {...set, 3};

      expect(identical(set, anotherSet), isFalse);
      expect(set.length, 2);
      expect(anotherSet.length, 3);

      // Mapに要素追加
      final map = {1: 'one', 2: 'two'};
      final anotherMap = {...map, 3: 'three'};

      expect(identical(map, anotherMap), isFalse);
      expect(map.length, 2);
      expect(anotherMap.length, 3);
    });

    test('新規のコレクションで削除', () {
      // Listから要素削除
      final list = [1, 2];
      final anotherList = [...list..remove(1)];

      expect(identical(list, anotherList), isFalse);
      expect(list.length, 1);
      expect(anotherList.length, 1);

      // Setから要素削除
      final set = {1, 2};
      final anotherSet = {...set..remove(1)};

      expect(identical(set, anotherSet), isFalse);
      expect(set.length, 1);
      expect(anotherSet.length, 1);

      // Mapから要素削除
      final map = {1: 'one', 2: 'two'};
      final anotherMap = {...map..remove(1)};

      expect(identical(map, anotherMap), isFalse);
      expect(map.length, 1);
      expect(anotherMap.length, 1);
    });

    test('インデックスで削除して、新規List', () {
      // 指定したインデックスで要素を削除して新しいListを作成する関数
      List<int> removeByIndex(List<int> list, int index) {
        return [
          ...list.getRange(0, index),
          ...list.getRange(index + 1, list.length)
        ];
      }

      // Listから要素削除
      final list = [0, 1, 2, 3];
      expect(removeByIndex(list, 0), equals([1, 2, 3]));
      expect(removeByIndex(list, 1), equals([0, 2, 3]));
      expect(removeByIndex(list, 2), equals([0, 1, 3]));
      expect(removeByIndex(list, 3), equals([0, 1, 2]));

      expect(removeByIndex([1], 0), equals([]));
    });
  });
}