導入
roadmap.shのFlutterのAdvanced Dart
の項目を引き続き進めていきます。
今回は、Collections
です。
Collections
概要
Dartでは、コレクションを使ってオブジェクトのグループを保存したり操作したりします。Dartで利用可能なコレクションには、次のような種類があります。
-
List
- 順序を持つ
- インデックスでアクセス可能
-
Set
- 順序を持たない
- 要素が重複しない
-
Map
- キーと値のペア
-
Queue
- 順序を持つ
- 先入れ先出し
-
Stack
- 順序を持つ
- 後入れ先出し
これらのコレクションは、データを効率的に保存および操作するための組み込みデータ構造です。ユーザーデータの保存、状態の管理、アルゴリズムの整理など、さまざまな場面で活用できます。
List
・Set
・Map
はよく登場するのですが、逆にQueue
やStack
はあまり登場しない気がします。
Collectionsの基本
Lists
ほぼすべてのプログラミング言語で最も一般的なコレクションは、配列やオブジェクトの順序付きグループです。Dartでは、配列はList
オブジェクトとなります。
Dartのリストリテラルは、四角括弧[]
で囲まれた、カンマ区切りの式や値のリストで表されます。
var list = [1, 2, 3];
Dartは上記のlist
がList<int>
型であると推論してくれます。このリストに整数以外のオブジェクトを追加しようとすると、アナライザーや実行時にエラーが発生します。
Dartのコレクションリテラルの最後の項目の後にカンマを追加できます。この末尾のカンマはコレクションには影響しませんが、コピーペーストによるエラー、Gitでの差分を考慮すると、利便性が高いので、私はつけることをおすすめしています。
var list = [
'Car',
'Boat',
'Plane',
];
当然ではありますが、リストのインデックスは0から始まります。0が最初の値のインデックスであり、list.length - 1
が最後の値のインデックスです。
リストの長さは.length
プロパティを使用して取得でき、リストの値には添字演算子[]
でアクセスできます。
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);
list[1] = 1;
assert(list[1] == 1);
コンパイル時定数のリストを作成するには、リストリテラルの前にconst
を追加します。
var constantList = const [1, 2, 3];
constantList[1] = 1; // この行はエラーになります。
Sets
Dartのセットは、順序付けされていないユニークなアイテムのコレクションです。DartではセットリテラルとSet
型でセットをサポートしています。
以下はセットリテラルを使ったケースです。
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
Dartは、このhalogens
がSet<String>
型であると推論します。
空のセットを作成するには、型引数の付いた{}
を使用するか、{}
をSet
型の変数に代入します:
var names1 = <String>{};
Set<String> names2 = {}; // これでもセットとして動作します。
var names3 = {}; // これはセットではなく、マップを作成します。
セットとマップの違い
マップリテラルの構文はセットリテラルの構文に似ています。
マップリテラルが先に登場したため、{}
はデフォルトでMap
型になるらしいです。
{}
や代入される変数に型注釈を忘れると、DartはMap<dynamic, dynamic>
型のオブジェクトを作成します。
セットへのアイテムに対する操作
既存のセットにアイテムを追加するには、add()
またはaddAll()
メソッドを使用します。
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
セット内のアイテムの数を取得するには、length
を使用します。
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);
コンパイル時定数のセットを作成するには、Listと同じではありますが、セットリテラルの前にconst
を追加します。
final constantSet = const {
'fluorine',
'chlorine',
'bromine',
'iodine',
'astatine',
};
constantSet.add('helium'); // この行はエラーになります。
Maps
マップとはキーと値を関連付けるオブジェクトのことです。
キーと値の両方が任意のオブジェクト型を持つことができます。各キーは一度だけ出現し、同じ値を複数回使用することはできます。Dartでは、マップリテラルとMap
型を使用してマップをサポートしています。
以下に、マップリテラルを使用したDartのマップの例を示します。
var gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
毎回毎回ですが、Map
においても型推論が実行されます。
gifts
はMap<String, String>
型であり、nobleGases
がMap<int, String>
型であると推論します。
同じオブジェクトをMap
コンストラクタを使用して作成することもできます。
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';
C#やJavaのような言語では、new Map()
のように記述しますが、Flutterでは上記のようにMap()
を使用することができます。Dartでは、new
キーワードはオプションです。
既存のマップに新しいキーと値のペアを追加するには、添字代入演算子[]=
を使用します。
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // キーと値のペアを追加
マップから値を取得するには、添字演算子[]
を使用します。
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');
存在しないキーを探すと、null
が返却されます。
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);
マップ内のキーと値のペアの数を取得するには、length
を使用します。
List・Setと同じく、length
で良いのは、Dartの良いところのように感じます。
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);
コンパイル時定数のマップを作成するには、マップリテラルの前にconst
を追加します。
final constantMap = const {
2: 'helium',
10: 'neon',
18: 'argon',
};
constantMap[2] = 'Helium'; // この行はエラーになります。
演算子 (Operators)
スプレッド演算子 (Spread Operators)
Dartは、リスト、マップ、およびセットリテラルでスプレッド演算子(...
)とnull許容スプレッド演算子(...?
)をサポートしています。スプレッド演算子を使用すると、コレクションに複数の値を簡潔に挿入できます。
めちゃくちゃ便利です。
以下の例では、スプレッド演算子...
を使用して、リスト内のすべての値を別のリストに挿入しています。
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
スプレッド演算子の右側の式がnullである可能性がある場合、null許容スプレッド演算子...?
を使用して例外を回避できます。
var list2 = [0, ...?list];
assert(list2.length == 1);
制御フロー演算子 (Control-flow Operators)
Dartは、リスト、マップ、およびセットリテラルで使用するためのif
とfor
のコレクションを提供しています。これらの演算子を使用して、条件付きや反復によってコレクションを作成できます。
以下の例では、if
コレクションを使用して、3つまたは4つのアイテムを持つリストを作成しています。
var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];
// Dartでは、コレクションリテラル内で`if-case`もサポートしています。
var nav2 = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];
機能としては良いと思いますが、大規模開発や複数人での開発においては、可読性の観点から、使用は控えた方がよいと、個人的には思います。
次の例は、for
コレクションを使用して、アイテムを別のリストに追加する前に操作しています。
var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');
Generics
みんな大好きGenerics
です。
正直私は使いこなせてはいません。
さて、内容に入っていきます。
基本的な配列型であるListのAPIドキュメントを見てみると、みなさんご存知の通り、その型が実際にはList<E>
となっています。この<...>
の記法は、Listがジェネリック(またはパラメータ化された)型であることを示します。一般的に、型変数はE
、T
、S
、K
、V
などの一文字で表されることが多いです。
なぜジェネリックを使うのか?
ジェネリックは型の安全性を確保するために必要なことが多いですが、単にコードが正しく動作するだけではなく、他にもさまざまなメリットがあります。
- より良いコードを記述していくことができる
- コードの重複を減らすことができる
例えば、あるリストに文字列だけを含めたい場合、そのリストをList<String>
(「文字列のリスト」と読みます)として宣言できます。これにより、自分自身や他のプログラマー、またはツールが、非文字列をそのリストに追加しようとしたときに、以下の例のように誤りであることを検出できます。
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // エラー
この例は当然と言えば、当然ですが、その当然を生み出しているのが、ジェネリクスということになります。
では、コードの重複を減らすとはどういうことでしょうか?
ジェネリックを使えば、さまざまな型に対して単一のインターフェースや実装を共有でき、なおかつ静的解析の恩恵を受けることができます。
例えば、以下のようにオブジェクトのキャッシュ用のインターフェースを作成することを考えます。
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
次に、このインターフェースの文字列専用バージョンが必要だと気づき、別のインターフェースを作成します。
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
さらに、数値専用バージョンが必要だと気づくかもしれません...
このパターンが、他の型に対して続いていくことが予想できます。
ジェネリック型を使えば、これらすべてのインターフェースを作成する手間を省くことができます。
どうするかというと、それぞれでインターフェースを作成するのではなく、型パラメータを取る単一のインターフェースを作成します。
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
このコードでは、T
は仮の型として機能します。これは、我々開発者が後で定義する型のプレースホルダーだと思ってください。
コレクションリテラルの使用
List
、Set
、Map
のリテラルには型パラメータを指定することができます。パラメータ化されたリテラルは通常のリテラルと同様ですが、List
やSet
の場合は<型>
を、Map
の場合は<キーの型, 値の型>
を設定します。
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'
};
コンストラクタでのパラメータ化された型の使用
コンストラクタを使用する際に、型を指定するには、クラス名の直後の<...>
内に型を入れます。
var nameSet = Set<String>.from(names);
以下の例では、キーが整数で、値がView
型であるMap
を定義しています。
var views = Map<int, View>();
ジェネリックコレクションとその中の型
Dartのジェネリック型は、具象化(reified) されており、実行時に型情報を保持します。例えば、コレクションの型をテストすることができます。
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
Javaでは、ジェネリクスは消去(erasure)を使用しており、実行時にジェネリック型パラメータが削除されます。Javaでは、オブジェクトがList
かどうかをテストできますが、それがList<String>
であるかどうかをテストすることはできません。
パラメータ化された型の制限
ジェネリック型を実装する際に、引数として提供できる型を特定の型のサブタイプに制限したい場合があります。この場合、extends
を使用して制限をかけることができます。
一般的な使用例として、デフォルトでnullが許容されたObject?
である型を、Object
のサブタイプにすることで、非null型に制限することが挙げられます。
class Foo<T extends Object> {
// Tとして提供される型は、非null型でなければなりません。
}
Object
以外の型にもextends
を使用できます。以下は、SomeBaseClass
を拡張し、T
型のオブジェクトでSomeBaseClass
のメンバーを呼び出せるようにする例です。
class Foo<T extends SomeBaseClass> {
// 実装はここに書かれます...
String toString() => "Instance of 'Foo<$T>'";
}
class Extender extends SomeBaseClass {...}
SomeBaseClass
またはそのサブタイプをジェネリック引数として指定することができます。
// SomeBaseClassをジェネリック引数に指定
var someBaseClassFoo = Foo<SomeBaseClass>();
// SomeBaseClassのサブタイプであるExtenderをジェネリック引数に指定
var extenderFoo = Foo<Extender>();
ジェネリック引数を指定しなくても利用可能が、適切でないかたをジェネリック引数に設定するとエラーになります。
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'
var boo = Foo<Object>(); // エラー
ジェネリックメソッドの使用
メソッドや関数でも、型引数を利用することができます。
T first<T>(List<T> ts) {
// 初期処理やエラーチェックを行い、その後...
T tmp = ts[0];
// 追加のチェックや処理を行い...
return tmp;
}
この例では、first
関数におけるジェネリック型パラメータ<T>
を使用することで、型引数T
を以下の複数の場所で利用できます。
- 関数の戻り値の型として (
T
) - 引数の型として (
List<T>
) - ローカル変数の型として (
T tmp
)
このように、ジェネリックメソッドを使うことで、柔軟で再利用可能なコードを記述できます。