こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 28 回です。Weekly Report については、第 1 回の記事を参照してください。
制約にも相続税
Kotlin では Array<Int>
や List<Int>
よりも、IntArray
(Java の int[]
に相当) の方が、boxing が不要といった理由で効率的になることが多いです。ただし、IntArray
は可変である上に、List
のユーティリティをそのまま使うことができません。そこで、ある開発者が以下のように ImmutableIntList
というクラスを作りました。このクラスは、List
のインターフェースをもつ IntArray
のラッパーで、不変であることを保証できます。(注: Kotlin の List
は Java の List
とは異なり、set
や add
といったメソッドは持ちません。)
class ImmutableIntList(vararg values: Int): List<Int> {
// It's safe to assign `values` because `vararg` does not keep the reference to the `*array` argument.
private val valueArray : IntArray = values
override operator fun get(index: Int): Int = valueArray[index]
override val size: Int = valueArray.size
... snip
}
そして、別の開発者が ImmutableIntList
の様々なバリエーションを作りたいと考えたとします。例えば、要素がソートされていることを保証するような、ImmutableSortedIntList
といったものです。以下の実装では ImmutableIntList
を継承可能 (open class
) にし、その子クラスとして ImmutableSortedIntList
を定義しています。
open class ImmutableIntList(vararg values: Int): List<Int> {
protected val valueArray : IntArray = values
override operator fun get(index: Int): Int = valueArray[index]
... snip
}
class ImmutableSortedIntList(vararg values: Int) :
ImmutableIntList(*values.sorted().toIntArray())
class AnotherImmutableIntList(...): ImmutableIntList() {
... // may touch `valueArray` here.
}
この構造に何か問題はありますか?
不変性への相続税
不変であることを保証したい場合は、継承不能にしたほうがよいです。あるクラスで Immutable
と名付けても、継承可能な場合 は子クラスで可変にできてしまいます。
特に ImmutableIntList
は汎用的で広く使われるような名前がつけられているため、思わぬパッケージやモジュールで「誤った継承がされる」可能性があります。たとえば、以下のように不変性を破る子クラス、見知らぬどこかで作られてしまうかもしれません。
class MutableIntList(...): ImmutableIntList(...) {
operator fun set(index: Int, value: Int) {
valueArray[index] = value
}
...
}
val mutableIntList = MutableIntList(...)
// Although this is an `ImmutableIntList`, it's actually mutable
// because it's modifiable by accessing `mutableIntList`.
val immutableIntList: ImmutableIntList = mutableIntList
これを防ごうとして valueArray
を private
にしても、不変性を維持するためには不十分です。get
関数がまだオーバーライド可能であるため、valueArray
を無視した実装ができてしまいます。
class IntListWithMutableSuffix(...): ImmutableIntList(...) {
var suffix: Int = 0
override operator fun get(index: Int): Int =
if (index != super.size) super[get] else suffix
...
}
不変性に限った話ではないですが、基本的・汎用的なクラスを継承可能にする場合、どのメンバーをオーバーライド可能にするかは慎重に検討する必要があります。また、継承可能にするのではなく、継承の関係にない独立した型を定義することも検討してください。
不変性と継承関係
一般に、可変なオブジェクトのクラスと不変なオブジェクトのクラスは、互いに継承関係を持たないようにすべきです。
- 可変が 不変を継承する場合: 先述のように不変性の制約が破られる。
- 不変が可変を継承する場合: 変更するメソッド (
add
/set
など) が不変なオブジェクトのクラスからも呼び出せる必要があるため、実行時エラーが必須になる (例: Guava のImmutableList
)。
可変と不変の共通の親クラスが必要な場合は、「読み取り専用 (read-only, unmodifiable)」として作るのが良いでしょう。(Kotlin の List もそのような継承構造になっています。)
- 可変が読み取り専用を継承する場合:問題なし。親クラスの「メソッド」は子クラスのメソッドのサブセットになっている。(子クラスだけが
set
/add
などを持ち、親クラスだけで有効なメソッドはない) - 不変が読み取り専用を継承する場合:問題なし。親クラスの「制約」は子クラスの制約のサブセットになっている。(子クラスだけが不変というを持ち、親クラスだけの制約はない)
一言まとめ
不変性を保証したい場合、継承不能にしたほうがよい。
キーワード: immutability
, inheritance
, override