こんにちは、エキサイトでアプリアンジニアをしている岡島です。
今回は、BuiltListを使う機会があったので、調べたことについて共有したいと思います。
BuiltListとBuilt Collectionについて
BuiltListは、DartのBuilt Collection Libraryに用意されている型です。
Built Collectionで用意されているコレクションは、Dartのcoreライブラリで用意されているコレクションとは異なり、次のような特徴を持ちます。(built_collection library - Dart APIより和訳)
- 変更不可(Immutable)
- 比較可能(Comparable)
- ハッシュ可能(Hashable)
- nullを拒否(Reject nulls)
- ジェネリクス型パラメータが必要(Require generic type parameters)
- 間違った型の要素を拒否(Reject wrong-type elements)
- 不要なコピーを避けるためCopy-on-Writeを使用(Copy-on-write to avoid copying unnecessarily)
Dartで注意が必要な参照渡し
Dartのcollections(List、Map、Set)は参照渡しとなるので注意が必要です。
以下のように、リストを別の変数に代入した場合、同じメモリを参照するため、一方のリストを変更すると両方のリストに影響が出てしまいます。
List<int> originData = [1, 2, 3];
final copiedData = originData;
copiedData[0] = 99;
print(originData);
print(copiedData);
BuiltListで参照渡しの問題を防ぐ
BuiltListは変更不可なため、このようなバグを防ぐことができます。
BuiltList<int> originData = BuiltList<int>([1, 2, 3]);
BuiltList<int> copiedData = originData;
BuiltList<int> modifiedData = originData.rebuild((b) => b[0] = 99);
print(originData);
print(modifiedData);
Built Collectionの特徴
BuiltListとBuilt Collectionについての章で述べたBuilt Collectionの特徴についてもう少し詳しく見ていきます。
変更不可(Immutable)
Built Collectionは変更不可であり、一度作成した後に変更することができません。データの変更は、rebuildメソッドを使って新しいインスタンスを作成することで行います。
比較可能(Comparable)
List<int> list1 = [1, 2, 3];
List<int> list2 = [1, 2, 3];
print(list1 == list2);
普通のListの場合、メモリの参照を比較するので、list1とlist2は異なるインスタンスとして認識されます。
BuiltList<int> list1 = BuiltList<int>([1, 2, 3]);
BuiltList<int> list2 = BuiltList<int>([1, 2, 3]);
print(list1 == list2);
Built Collectionは、deep comparisonを行うため、内容が同じ場合は等しいと判定されます。
ハッシュ可能(Hashable)
deep hashCodeが計算されてキャッシュされるようです。
プログラムでみると分かりやすいと思うのでサンプルコードを書いてみました。
含まれているデータの内容を見て、ハッシュコードが計算されるので、
データが同じであればハッシュコードも同じです。
普通のList:
List<List<int>> coreList1 = [
[1, 2, 3]
];
List<List<int>> coreList2 = [
[1, 2, 3]
];
print(coreList1.hashCode == coreList2.hashCode);
BuiltList:
BuiltList<BuiltList<int>> builtList1 = BuiltList<BuiltList<int>>([
BuiltList<int>([1, 2, 3])
]);
BuiltList<BuiltList<int>> builtList2 = BuiltList<BuiltList<int>>([
BuiltList<int>([1, 2, 3])
]);
print(builtList1.hashCode == builtList2.hashCode);
nullを拒否(Reject nulls)
nullの要素が入っていると、エラーが発生します。
try {
BuiltList<int> listWithNull = BuiltList<int>([1, null, 3]);
} catch (e) {
print(e);
}
Built Collectionでは型パラメータを指定する必要があります。
Listでは型を指定せずに宣言すると警告が出ず、任意のタイプのListになるため、バグが起こりやすくなります。
List mixedList = [1, "two", 3.0];
しかし、BuiltListなどのBuilt Collectionでは、型を指定しないとエラーになるため、このような問題は発生しません。
BuiltList mixedList = [1, "two", 3.0]; // エラー
間違った型の要素を拒否(Reject wrong-type elements)
違う型の要素を追加しようとするとエラーが発生します。
BuiltList<int> numbers = BuiltList<int>([1, 2, 3]);
numbers.rebuild((b) => b..add("string"));
不要なコピーを避けるためCopy-on-Writeを使用(Copy-on-write to avoid copying unnecessarily)
「Copy-on-Write」の原則を使用しており、データが変更されるときのみ新しいインスタンスを作成します。これにより、メモリ使用量が最適化されます。
以下の例で示すように、identicalで同一オブジェクトであるかを判定すると、値が同じものは同一オブジェクトになっています。
BuiltList<int> originalList = BuiltList<int>([1, 2, 3]);
BuiltList<int> sameList = originalList.rebuild((b) => b);
print(identical(originalList, sameList)); // true (同じインスタンス)
BuiltList<int> modifiedList = originalList.rebuild((b) => b..add(4));
print(identical(originalList, modifiedList)); // false (新しいインスタンスが作成される)
最後に
今回はBuilt Collectionのライブラリについて詳しく見てみました。
この記事がBuilt CollectionやBuiltListの理解を深める助けになれば幸いです。