こんにちは。コミュニケーションアプリ「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