Roadmap.shで学ぶFlutter - Advanced Dart - [Collections]

# 導入
[roadmap.shのFlutter](https://roadmap.sh/flutter)の`Advanced Dart`の項目を引き続き進めていきます。

今回は、`Collections`です。

# Collections
## 概要
Dartでは、コレクションを使ってオブジェクトのグループを保存したり操作したりします。Dartで利用可能なコレクションには、次のような種類があります。

- **List**
    - 順序を持つ
    - インデックスでアクセス可能
- **Set**
    - 順序を持たない
    - 要素が重複しない
- **Map**
    - キーと値のペア
- **Queue**
    - 順序を持つ
    - 先入れ先出し
- **Stack**
    - 順序を持つ
    - 後入れ先出し

これらのコレクションは、データを効率的に保存および操作するための組み込みデータ構造です。ユーザーデータの保存、状態の管理、アルゴリズムの整理など、さまざまな場面で活用できます。
`List`・`Set`・`Map`はよく登場するのですが、逆に`Queue`や`Stack`はあまり登場しない気がします。

## Collectionsの基本

https://dart.dev/language/collections

### Lists

ほぼすべてのプログラミング言語で最も一般的なコレクションは、配列やオブジェクトの順序付きグループです。Dartでは、配列は`List`オブジェクトとなります。

Dartのリストリテラルは、四角括弧`[]`で囲まれた、カンマ区切りの式や値のリストで表されます。

```dart
var list = [1, 2, 3];
```

:::note info
Dartは上記の`list`が`List<int>`型であると推論してくれます。このリストに整数以外のオブジェクトを追加しようとすると、アナライザーや実行時にエラーが発生します。
:::

Dartのコレクションリテラルの最後の項目の後にカンマを追加できます。この末尾のカンマはコレクションには影響しませんが、コピーペーストによるエラー、Gitでの差分を考慮すると、利便性が高いので、私はつけることをおすすめしています。

```dart
var list = [
  'Car',
  'Boat',
  'Plane',
];
```

当然ではありますが、リストのインデックスは0から始まります。0が最初の値のインデックスであり、`list.length - 1`が最後の値のインデックスです。
リストの長さは`.length`プロパティを使用して取得でき、リストの値には添字演算子`[]`でアクセスできます。

```dart
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);
```

コンパイル時定数のリストを作成するには、リストリテラルの前に`const`を追加します。

```dart
var constantList = const [1, 2, 3];
constantList[1] = 1; // この行はエラーになります。
```

### Sets

Dartのセットは、**順序付けされていないユニークなアイテムのコレクション**です。Dartではセットリテラルと`Set`型でセットをサポートしています。

以下はセットリテラルを使ったケースです。

```dart
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
```

:::note info
Dartは、この`halogens`が`Set<String>`型であると推論します。
:::

空のセットを作成するには、型引数の付いた`{}`を使用するか、`{}`を`Set`型の変数に代入します:

```dart
var names1 = <String>{};
Set<String> names2 = {}; // これでもセットとして動作します。

var names3 = {}; // これはセットではなく、マップを作成します。
```

#### セットとマップの違い
マップリテラルの構文はセットリテラルの構文に似ています。
マップリテラルが先に登場したため、`{}`はデフォルトで`Map`型になるらしいです。
`{}`や代入される変数に型注釈を忘れると、Dartは`Map<dynamic, dynamic>`型のオブジェクトを作成します。

#### セットへのアイテムに対する操作

既存のセットにアイテムを追加するには、`add()`または`addAll()`メソッドを使用します。

```dart
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
```

セット内の**アイテムの数**を取得するには、`length`を使用します。

```dart
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);
```

コンパイル時定数のセットを作成するには、Listと同じではありますが、セットリテラルの前に`const`を追加します。

```dart
final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};

constantSet.add('helium'); // この行はエラーになります。
```

### Maps

マップとはキーと値を関連付けるオブジェクトのことです。
キーと値の両方が任意のオブジェクト型を持つことができます。各キーは一度だけ出現し、同じ値を複数回使用することはできます。Dartでは、マップリテラルと`Map`型を使用してマップをサポートしています。

以下に、マップリテラルを使用したDartのマップの例を示します。

```dart
var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};
```

:::note info
毎回毎回ですが、`Map`においても型推論が実行されます。
`gifts`は`Map<String, String>`型であり、`nobleGases`が`Map<int, String>`型であると推論します。
:::

同じオブジェクトを`Map`コンストラクタを使用して作成することもできます。

```dart
var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
```

:::note warn
C#やJavaのような言語では、`new Map()`のように記述しますが、Flutterでは上記のように`Map()`を使用することができます。Dartでは、`new`キーワードはオプションです。
:::

既存のマップに新しいキーと値のペアを追加するには、添字代入演算子`[]= `を使用します。

```dart
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // キーと値のペアを追加
```

マップから値を取得するには、添字演算子`[]`を使用します。

```dart
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');
```

存在しないキーを探すと、`null`が返却されます。

```dart
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);
```

マップ内のキーと値のペアの数を取得するには、`length`を使用します。
List・Setと同じく、`length`で良いのは、Dartの良いところのように感じます。

```dart
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);
```

コンパイル時定数のマップを作成するには、マップリテラルの前に`const`を追加します。

```dart
final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

constantMap[2] = 'Helium'; // この行はエラーになります。
```

### 演算子 (Operators)

#### スプレッド演算子 (Spread Operators)

Dartは、リスト、マップ、およびセットリテラルでスプレッド演算子（`...`）とnull許容スプレッド演算子（`...?`）をサポートしています。スプレッド演算子を使用すると、コレクションに複数の値を簡潔に挿入できます。
めちゃくちゃ便利です。

以下の例では、スプレッド演算子`...`を使用して、リスト内のすべての値を別のリストに挿入しています。

```dart
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
```

スプレッド演算子の右側の式がnullである可能性がある場合、null許容スプレッド演算子`...?`を使用して例外を回避できます。

```dart
var list2 = [0, ...?list];
assert(list2.length == 1);
```

#### 制御フロー演算子 (Control-flow Operators)

Dartは、リスト、マップ、およびセットリテラルで使用するための`if`と`for`のコレクションを提供しています。これらの演算子を使用して、条件付きや反復によってコレクションを作成できます。

以下の例では、`if`コレクションを使用して、3つまたは4つのアイテムを持つリストを作成しています。

```dart
var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];

// Dartでは、コレクションリテラル内で`if-case`もサポートしています。
var nav2 = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];
```

機能としては良いと思いますが、大規模開発や複数人での開発においては、可読性の観点から、使用は控えた方がよいと、個人的には思います。

次の例は、`for`コレクションを使用して、アイテムを別のリストに追加する前に操作しています。

```dart
var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');
```

## Generics

https://dart.dev/language/generics

みんな大好き`Generics`です。
正直私は使いこなせてはいません。

さて、内容に入っていきます。
基本的な配列型であるListのAPIドキュメントを見てみると、みなさんご存知の通り、その型が実際には`List<E>`となっています。**この`<...>`の記法は、Listがジェネリック（またはパラメータ化された）型である**ことを示します。一般的に、型変数は`E`、`T`、`S`、`K`、`V`などの一文字で表されることが多いです。

### なぜジェネリックを使うのか？

ジェネリックは**型の安全性を確保するため**に必要なことが多いですが、単にコードが正しく動作するだけではなく、他にもさまざまなメリットがあります。

- より良いコードを記述していくことができる
- コードの重複を減らすことができる

例えば、あるリストに文字列だけを含めたい場合、そのリストを`List<String>`（「文字列のリスト」と読みます）として宣言できます。これにより、自分自身や他のプログラマー、またはツールが、非文字列をそのリストに追加しようとしたときに、以下の例のように**誤りであることを検出**できます。

```dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // エラー
```
この例は当然と言えば、当然ですが、その当然を生み出しているのが、ジェネリクスということになります。

では、**コードの重複を減らす**とはどういうことでしょうか？
ジェネリックを使えば、さまざまな型に対して**単一のインターフェースや実装**を共有でき、なおかつ静的解析の恩恵を受けることができます。
例えば、以下のようにオブジェクトのキャッシュ用のインターフェースを作成することを考えます。

```dart
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
```

次に、このインターフェースの**文字列専用**バージョンが必要だと気づき、別のインターフェースを作成します。

```dart
abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
```

さらに、**数値専用**バージョンが必要だと気づくかもしれません...
このパターンが、他の型に対して続いていくことが予想できます。

ジェネリック型を使えば、これら**すべてのインターフェースを作成する手間を省く**ことができます。
どうするかというと、それぞれでインターフェースを作成するのではなく、型パラメータを取る単一のインターフェースを作成します。

```dart
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}
```

このコードでは、`T`は仮の型として機能します。これは、我々開発者が後で定義する型のプレースホルダーだと思ってください。

### コレクションリテラルの使用

`List`、`Set`、`Map`のリテラルには型パラメータを指定することができます。パラメータ化されたリテラルは通常のリテラルと同様ですが、`List`や`Set`の場合は`<型>`を、`Map`の場合は`<キーの型, 値の型>`を設定します。

```dart
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};
```

### コンストラクタでのパラメータ化された型の使用

コンストラクタを使用する際に、型を指定するには、クラス名の直後の`<...>`内に型を入れます。

```dart
var nameSet = Set<String>.from(names);
```

以下の例では、キーが整数で、値が`View`型である`Map`を定義しています。

```dart
var views = Map<int, View>();
```

### ジェネリックコレクションとその中の型

Dartのジェネリック型は、具象化(reified) されており、実行時に型情報を保持します。例えば、コレクションの型をテストすることができます。

```dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
```

:::note warn
Javaでは、ジェネリクスは消去(erasure)を使用しており、実行時にジェネリック型パラメータが削除されます。Javaでは、オブジェクトが`List`かどうかをテストできますが、それが`List<String>`であるかどうかをテストすることはできません。
:::

### パラメータ化された型の制限

ジェネリック型を実装する際に、引数として提供できる型を特定の型の**サブタイプに制限したい**場合があります。この場合、`extends`を使用して制限をかけることができます。

一般的な使用例として、デフォルトでnullが許容された`Object?`である型を、`Object`のサブタイプにすることで、非null型に制限することが挙げられます。

```dart
class Foo<T extends Object> {
  // Tとして提供される型は、非null型でなければなりません。
}
```

`Object`以外の型にも`extends`を使用できます。以下は、`SomeBaseClass`を拡張し、`T`型のオブジェクトで`SomeBaseClass`のメンバーを呼び出せるようにする例です。

```dart
class Foo<T extends SomeBaseClass> {
  // 実装はここに書かれます...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}
```

`SomeBaseClass`またはそのサブタイプをジェネリック引数として指定することができます。

```dart
// SomeBaseClassをジェネリック引数に指定
var someBaseClassFoo = Foo<SomeBaseClass>();
// SomeBaseClassのサブタイプであるExtenderをジェネリック引数に指定
var extenderFoo = Foo<Extender>();
```

ジェネリック引数を指定しなくても利用可能が、適切でないかたをジェネリック引数に設定するとエラーになります。

```dart
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

var boo = Foo<Object>(); // エラー
```

### ジェネリックメソッドの使用

メソッドや関数でも、型引数を利用することができます。

```dart
T first<T>(List<T> ts) {
  // 初期処理やエラーチェックを行い、その後...
  T tmp = ts[0];
  // 追加のチェックや処理を行い...
  return tmp;
}
```

この例では、`first`関数におけるジェネリック型パラメータ`<T>`を使用することで、型引数`T`を以下の複数の場所で利用できます。

- 関数の戻り値の型として (`T`)
- 引数の型として (`List<T>`)
- ローカル変数の型として (`T tmp`)

このように、ジェネリックメソッドを使うことで、柔軟で再利用可能なコードを記述できます。