こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 22 回です。Weekly Report については、第 1 回の記事を参照してください。
To equal, or not to equal
Java や Kotlin では、equals
をオーバーライドすることで「構造の等価性 (structural equality)」を定義することができます。(ただし、equals
をオーバーライドするときは、hashCode
もオーバーライドする必要があります。)
以下のような、ユーザプロファイルの UI のデータモデルがあるとしましょう。このデータモデルでは、独自の equals
と hashCode
を定義しています。
このコードになにか問題点はありますか?
オレンジもりんごも同じ「フルーツ」なので同じ「もの」?
一般論として、equals
で一部のプロパティだけを比較するのは避けるべきです。equals
(equality) は通常、「同一性 (identity, referential equality, same object)」か「等価性/同値性 (equivalence)」のどちらかを示すべきです。
先程の UserProfileViewData.equals
の実装が示しているものは、同一性でも等価性でもありません。そのため、思わぬバグを引き起こす可能性があります。
UserProfileViewData
の情報が変わる度に、UI も更新することを想定しましょう。UserProfileViewData
のインスタンスは、以下のような “observable” なものとして提供されるとします。
- Kotlin コルーチンの
StateFlow
- Kotlin コルーチンの
Flow
(distinctUntilChanged
付き) - Rx の
Observable
(distinctUntilChanged
付き) - Android の
LiveData
(distinctUntilChanged
付き)
これら 4 つの “observable” は、UserProfileViewData
が変わる度にそのインスタンスを出力するのですが、「UserProfileViewData
が変わったか」の判定に equals
を使います。つまり、1 つ前のインスタンスと equals
で比較をして true
ならば無視し、false
なら出力するという挙動です。したがって、nickname
や statusMessage
が更新されたとしてもuserId
が等しいならば equals
も true
となるため、UI が更新されません。これは、nickname
を更新しても画面に反映されないというバグを引き起こします。
equals
(equality) の定義は、以下の 2 つのどちらかであるべきです。
- 同一性 (identity): 2 つのオブジェクト(やその参照)が同一であるときに
true
。Java や Kotlin の場合はObject
やAny
のequals
をそのまま使う。 - 等価性/同値性 (equivalence): すべてのプロパティやフィールドが等価/同値の場合に
true
。すべてのプロパティやフィールドに等価/同値が定義されている必要がある。Kotlin の場合はdata
として簡単に実装できる。
もし、一部のプロパティだけを比較するような関数が必要なときは、equals
とは別の関数として定義するべきです。以下の例では、userId
のみを比較する関数を独自に定義しています。
補足 1: Kotlin の data class
Kotlin の data class
におけるデフォルトの equals
の実装は、コンストラクタパラメータのプロパティ以外は無視することに留意してください。
補足 2: 例外ケース
等価性/同値性を定義する際に、例外的にいくつかのプロパティを無視することがあります。計算結果のキャッシュやメモは、その典型的な例です。以下の Factorial
は、n
の階乗を計算する求めるクラスで、一度計算した値を cachedValue
として保持します。この cachedValue
は equals
で比較されていません。しかし、キャッシュの有無による振る舞いの差を無視できる場合(計算時間の差などを無視して良い場合など)はこの実装で問題ないでしょう。
補足 3: 表現か、実際の値か、それが問題だ
ここまでの説明では、まるで等価性/同値性は当然に定義できるものであるかのように扱いました。しかし実際には、等価性/同値性は見方によっても変わるため、注意深く定義する必要があります。有理数 Rational
が以下のように定義されていることを想定しましょう。
numerator
は符号付きの分子を示し、denominator
が分母を示します。ここで、Rational(1, 2)
(1/2) と Rational(2, 4)
(2/4) の比較について考えます。
この結果が true
であるべきか、false
であるべきかは Rational
が何を示すかによって異なります。
- 表示のためのモデルの場合:
false
を返すべき。例えば"1/2 + 2/4 = 1"
を UI に出力する場合、1/2
と2/4
は別のもの。 - 計算のためのモデルの場合:
true
を返すべき。例えば、1/4 + 1/4
の計算結果は1/2
と「同じ」である必要がある。
一言まとめ
equals
が同一性 (identity) と等価性/同値性 (equivalence) のどちらを示しているのかを明確にする
キーワード: identity
, equivalence
, equals