はじめに
Composeで List を扱うとき、mutableStateListOf で生成される SnapshotStateList を利用することは多いでしょう。
しかし SnapshotStateList の toString / equals / hashCode の挙動は、一般的な List とは異なります。この違いを理解していないと、比較処理などで意図しない挙動に遭遇します。
本記事では、次の観点について解説します:
- Kotlinの
Listについて -
SnapshotStateListの性質 - なぜ
toString/equals/hashCodeで差が出るのか -
SnapshotStateListの扱い方
※ 同様の注意点は SnapshotStateSet や SnapshotStateMap にもあります。
List の仕様
List は要素の順序を保持するコレクション型であり、interface として定義されています。そのため、具体的な実装は複数存在し、独自実装も可能です。
Kotlinのドキュメントでは、List の toString / equals / hashCode について明確な記述があります。
要点は次の通りです:
-
toStringは要素を順序通りに並べた文字列表現を返す -
equalsは要素数・順序・各要素のequalsに基づいて比較する -
hashCodeは各要素のhashCodeを組み合わせて算出する
ドキュメントの原文
List.toStringshould return a string containing string representation of contained elements in exact same order these elements are stored within the list.List.equalsshould consider two lists equal if and only if they contain the same number of elements and each element in one list is equal to an element in another list at the same index. Unlike some otherequalsimplementations,List.equalsshould consider two lists equal even if they are instances of different classes; the only requirement here is that both lists have to implementListinterface.List.hashCodeshould be computed as a combination of elements' hash codes using the following algorithm:var hashCode: Int = 1 for (element in this) hashCode = hashCode * 31 + element.hashCode()
つまり、List は構造的等価(structural equality)で比較されます。
生成過程が異なっていても、要素が同じであれば等しいと判定されます:
val list1 = mutableListOf("a", "b")
val list2 = mutableListOf("a").also { it.add("b") }
list1 == list2 // true
SnapshotStateList とは何か
SnapshotStateList は androidx.compose.runtime.snapshots に含まれるComposeの状態管理向け MutableList 実装です。
この型はSnapshotシステムと連携するため、一般的な List 実装と違う挙動を持ちます。
toString / equals / hashCode の違い
SnapshotStateList では、一般的な List のような要素ベースの toString / equals / hashCodeを持ちません。代わりに、インスタンスの参照をベースにした実装になっています。
また、Kotlinの == 演算子は、左辺の equals メソッドを呼び出すため、SnapshotStateList と一般的な List を比較すると、一方は要素ベースで、もう一方は参照ベースで比較されることになります。
val list = listOf("a", "b")
val snapshotStateList = mutableStateListOf("a", "b")
list.toString() // [a, b]
snapshotStateList.toString() // SnapshotStateList(value=[a, b])@88539910
list.hashCode() // 4066
snapshotStateList.hashCode() // 88539910
list == snapshotStateList // true
snapshotStateList == list // false
一般的な List として扱うには
SnapshotStateList を要素ベースで扱いたい場合は、toList() によって得られる List を利用すると良いでしょう。
toList() で取得した List は読み取り専用として扱え、toString / equals / hashCode も一般的な List と同様に利用できます。また、取得後に SnapshotStateList 側を変更しても、すでに取得した List の内容は変わりません。
val snapshotStateList = mutableStateListOf("a", "b")
val immutableList = snapshotStateList.toList()
immutableList.toString() // [a, b]
immutableList.hashCode() // 4066
immutableList == listOf("a", "b") // true
まとめ
- 一般的な
ListのtoString/equals/hashCodeは要素ベースで扱われる -
SnapshotStateListのtoString/equals/hashCodeは参照ベースで扱われる - 要素比較が必要なら
toList()により、一般的なListと同様に扱う