データクラスの継承について考えてみた

Kotlin のデータクラスはスーパークラスになれません。例えば、これはコンパイルエラーになります。

```kotlin
data class Parent(val text: String)
data class Child(text: String, val number: Int) : Parent(text)
```

なので共通の性質を持つ複数のデータクラスを定義するのは諦めてたんですが、 **データクラスはサブクラスになれる** ことを（今更）知ったので、色々と試してみました。

## データクラスとなるための条件

公式ドキュメントは[こちら](https://kotlinlang.org/docs/reference/data-classes.html#data-classes)をご確認ください。データクラスであるための条件は以下の3点です。

* プライマリコンストラクタにパラメータが1つ以上なければならない
* プライマリコンストラクタの全てのパラメータは `val` または `var` として定義されなくてはならない
* `abstract`、`sealed`、`open`、`inner`、のどれにもなれない

<details><summary>Kotlin1.1以前の場合</summary>上記3つの条件に加えて、インターフェイスの実装は可能だがクラスの継承はできない、という条件がつきます。</details>

逆に言うとこれらの条件さえ満たせていればよいので、インターフェイス、通常のクラス、抽象クラスを継承した場合について考えてみました。

## 継承する際の共通点

コンストラクタにパラメータを持つクラスを継承する場合、サブクラスのコンストラクタでそれらを与える必要があります。しかしデータクラスでは、プライマリコンストラクタのパラメータが全て`var`か`val`でなくてはいけません。
例えば、以下はコンパイルエラーが発生します。

```kotlin
open class Parent(val text: String)
data class Child(text: String, val number: Int) : Parent(text)
```

~~なので、 **データクラスのスーパークラスになるには、パラメータの要らないコンストラクタが必要** ということになります。~~
そのため、データクラスが他のクラスを継承するときは **プライマリコンストラクタのパラメータを全て`var`か`val`にし、かつ何かしらの手段でスーパークラスのコンストラクタに値をセットする** 必要があります。
これには、以下のような方法が考えられます。

* スーパークラスに、パラメータを与えなくてもよいコンストラクタを定義する
* リテラルや関数などを使って、スーパークラスのコンストラクタに値をセットする
* データクラスのパラメータをスーパークラスのコンストラクタにセットする

以下にこれらの例を列挙してみました。

```kotlin
val sampleText: String
    get() = "This is sample."

fun createSampleText() = "This is sample."

open class Parent1(val text: String)

// リテラルや関数などでスーパークラスのコンストラクタに値をセットする
data class Child1a(val string: String, val number: Int) : Parent1("hello")
data class Child1b(val string: String, val number: Int) : Parent1(sampleText)
data class Child1c(val string: String, val number: Int) : Parent1(createSampleText())

// データクラスのパラメータをスーパークラスにセットする
data class Child1d(val string: String, val number: Int) : Parent1(string)

// 引数の要らないコンストラクタをスーパークラスに用意する
open class Parent2(val text: String = "")
data class Child2(val string: String, val number: Int) : Parent2()
```

## インターフェイスを実装する

メンバがない場合はこんな具合です。

```kotlin
interface Parent1 {
    fun test()
}

data class Child1(val text: String) : Parent1 {
    override fun test() { ... }
}
```

メンバがある場合、コンストラクタのパラメータにしても、ただのクラスメンバにしてもよいです。ただ、以下2点には改めて注意しましょう。

* プライマリコンストラクタは、最低1つのパラメータを持たなければならない
* 単なるメンバとしてオーバーライドしたフィールドは、自動生成される`equals`や`toString`などの対象から外れる

```kotlin
interface Parent1 {
    val number: Int
    val text: String
    fun test()
}

// コンストラクタのパラメータとしてオーバーライドした場合
data class Child1a(override val number: Int, override val text: String) : Parent1 {
    override fun test() { ... }
}

// 単なるメンバとしてオーバーライドした場合
// ここではゲッタとして定義しましたが、通常のフィールドでもよいです
data class Child1b(override val number: Int) : Parent1 {
    override val text: String
        get() = "number = $number"
    
    override fun test() { ... }
}
```

## 具象クラスを継承する

スーパークラスにプライマリコンストラクタがなく継承するメンバもない場合は、こんな具合です。

```kotlin
open class Parent2 {
    open fun test() { ... }
}

data class Child2(val number: Int) : Parent2() {
    override fun test() { ... }
}
```

何かしらのメンバをオーバーライドする場合、インターフェイスの場合とやり方は同じです。

```kotlin
open class Parent2 {
    open val number: Int = 0
    open val text: String = "super-class"
    open fun test() { ... }
}

// 片方だけプライマリコンストラクタのパラメータにした場合
data class Child2a(override val number: Int) : Parent2() {
    override val text: String = "sub-class"
    override fun test() { ... }
}

// 両方プライマリコンストラクタのパラメータにした場合
data class Child2b(override val number: Int, override val text: String) : Parent2() {
    override fun test() { ... }
}
```

スーパークラスのプライマリコンストラクタにパラメータがある場合は、スーパークラスでその値を与えなくてもよいようにするか、データクラス側で値をセットします。

```kotlin
open class Parent2(val text: String) {
    constructor() : this("Default Text")
    open val number: Int = 0
    open fun test() { ... }
}

// スーパークラスで定義した、値の要らないセカンダリコンストラクタを利用
data class Child2a(override val number: Int) : Parent2() {
    override fun test() { ... }
}

// データクラスのパラメータをセット
data class Child2b(val string: String, override val number: Int) : Parent2(string) {
    override fun test() { ... }
}
```

## 抽象クラスを継承する

インターフェイスを実装したり、具象クラスを継承する場合と変わりません。

```kotlin
abstract class Parent3 {
    abstract val value: String
    abstract fun test()
}

data class Child3(override val value: String) : Parent3() {
    override fun test() {}
}
```

## まとめ

個人的には、同じ性質を持つクラスには継承関係を持たせたいと考えています。
データクラスじゃない喩えでなくて恐縮ですが、Human というクラスがあったとして、性別を sex フィールドで持たせるよりも Human を継承した Male や Female といったクラスを定義したいのです。
ここに書いた方法を採用すると、「プライマリコンストラクタのパラメータが全く同じクラスがたくさんできる」という状況になる可能性があります。ただ、それでも他の手段よりデータクラスで実装したほうが適当な状況も、恐らくあると思います。
そんなことを考える方がいて、このエントリの内容がお役に立つことがあれば、とても嬉しく思います。

## 変更履歴

* 2020/04/14
    * 頂いたコメントをもとに、『継承する際の共通点』を修正しました。