🚔

structuralEquality vs referentialEquality vs neverEqual

2024/10/04に公開

はじめに

Jetpack Compose で mutableStateOf を使う機会は多いと思います。この関数の第2引数に SnapshotMutationPolicyを継承したpolicyを渡すことができます。runtime内で定義されているstructuralEqualityPolicy(), referentialEqualityPolicy(), neverEqualPolicy() の違いが気になったので見ていこうと思います。

fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() // これ
): MutableState<T> = createSnapshotMutableState(value, policy)

3つのpolicyの実装

policyのコードはここにあります。SnapshotMutationPolicy.kt in Andoid Code Search

3つのpolicyの違いはfun equivalent(a: T, b: T): Boolean の処理のようです。この関数はdocによると、状態の値の等価性を判定しているようです。

この判定結果を見てMutableStatevalueを書き換えているっぽいです。(多分)SnapshotMutableStateImpl in Andoid Code Search

1. structuralEqualityPolicy()

まずは、デフォルトで設定されているものです。

fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> =
    StructuralEqualityPolicy as SnapshotMutationPolicy<T>

private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> {
    override fun equivalent(a: Any?, b: Any?) = a == b

    override fun toString() = "StructuralEqualityPolicy"
}

a == bとあるので、(equals関数で)構造等価性をみているようです。Structural equality in Kotlin Doc

2. referentialEqualityPolicy()

fun <T> referentialEqualityPolicy(): SnapshotMutationPolicy<T> =
    ReferentialEqualityPolicy as SnapshotMutationPolicy<T>

private object ReferentialEqualityPolicy : SnapshotMutationPolicy<Any?> {
    override fun equivalent(a: Any?, b: Any?) = a === b

    override fun toString() = "ReferentialEqualityPolicy"
}

a === bとあるので、参照等価性(同じメモリ領域に値が書き込まれているか)をみているようです。Referential equality in Kotlin Doc

参考: LazyListItemProviderとかで使っているみたいです。

3. neverEqualPolicy()

fun <T> neverEqualPolicy(): SnapshotMutationPolicy<T> =
    NeverEqualPolicy as SnapshotMutationPolicy<T>

private object NeverEqualPolicy : SnapshotMutationPolicy<Any?> {
    override fun equivalent(a: Any?, b: Any?) = false

    override fun toString() = "NeverEqualPolicy"
}

falseを返しているのでいかなる場合も等しくないと判定しているみたいです。

動作確認

実際に3つのpolicyを使った場合のrecomposeの様子を見ていきます。

今回使用するコードは下記のものにします。余計なところは省略してます。以下留意点です。

  • デフォルトでstateにはUser("user1")が入っています。
  • 1つ目のボタンは、同じ値のインスタンス代入する場合(state = state)... (A)
  • 2つ目のボタンは、同じ値の別のインスタンスを代入する場合(state = User("user1"))... (B)
  • 3つ目のボタンは、異なる値のインスタンスを代入する場合(state = User("user2")) ... (C)
private data class User(val name: String = "user1")

@Composable
internal fun EqualityScreen() {

    Column {
        StructuralEquality(modifier = Modifier.weight(1f))
        ReferentialEquality(modifier = Modifier.weight(1f))
        NeverEqual(modifier = Modifier.weight(1f))
    }
}

@Composable
private fun HogeEquality(modifier: Modifier) {
    var state by remember {
        mutableStateOf(
            value = User("user1"),
            policy = structuralEqualityPolicy(),
            // policy = referentialEqualityPolicy(),
            // policy = neverEqualPolicy(),
        )
    }

    Column(
        modifier = modifier
            .fillMaxSize()
            .background(blue or pink or orange)
    ) {
        Text(policyのタイトル)
        
        Text("Name: ${state.name}")
        
        Button(
            onClick = { state = state },
            colors = blue or pink or orange,
        ) {
            Text("state = state")
        }

        Button(
            onClick = { state = User("user1") },
            colors = blue or pink or orange,
        ) {
            Text("state = User(\"user1\")")
        }

        Button(
            onClick = { state = User("user2") },
            colors = blue or pink or orange,
        ) {
            Text("state = User(\"user2\")")
        }
    }
}

実行すると以下のような画面になると思います。それぞれのボタンを押下した場合のrecomposeの様子をLayout Inspectorで見てみます。

1. structuralEqualityPolicy()

C のみrecomposeされていることがわかります。

2. referentialEqualityPolicy()

B, C のみrecomposeされていることがわかります。

3. neverEqualPolicy()

A, B, C の全てでrecomposeされていることがわかります。

まとめ

全ての場合に期待通りの挙動を確認することができました。(おしまい)

Discussion