こんにちは、エキサイトでアプリアンジニアをしている岡島です。 今回は、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); // [99, 2, 3] print(copiedData); // [99, 2, 3]
BuiltListで参照渡しの問題を防ぐ
BuiltListは変更不可なため、このようなバグを防ぐことができます。
BuiltList<int> originData = BuiltList<int>([1, 2, 3]); // originDataをコピーしても、内容を直接変更することはできない BuiltList<int> copiedData = originData; // copiedData[0] = 99; // これはエラーになる // rebuildを使って新しいリストを作成 BuiltList<int> modifiedData = originData.rebuild((b) => b[0] = 99); print(originData); // [1, 2, 3] - 元のリストは変更されない print(modifiedData); // [99, 2, 3] - 新しいリストが作成される
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); // false
普通のListの場合、メモリの参照を比較するので、list1とlist2は異なるインスタンスとして認識されます。
BuiltList<int> list1 = BuiltList<int>([1, 2, 3]); BuiltList<int> list2 = BuiltList<int>([1, 2, 3]); print(list1 == list2); // true
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); // false
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); // true
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]; // List<dynamic>となり、特定の操作をする時にエラーが発生する可能性がある。
しかし、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の理解を深める助けになれば幸いです。