9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-12

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

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

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

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

公式ドキュメントはこちらをご確認ください。データクラスであるための条件は以下の3点です。

  • プライマリコンストラクタにパラメータが1つ以上なければならない
  • プライマリコンストラクタの全てのパラメータは val または var として定義されなくてはならない
  • abstractsealedopeninner、のどれにもなれない
Kotlin1.1以前の場合上記3つの条件に加えて、インターフェイスの実装は可能だがクラスの継承はできない、という条件がつきます。

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

継承する際の共通点

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

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

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

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

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

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()

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

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

interface Parent1 {
    fun test()
}

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

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

  • プライマリコンストラクタは、最低1つのパラメータを持たなければならない
  • 単なるメンバとしてオーバーライドしたフィールドは、自動生成されるequalstoStringなどの対象から外れる
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() { ... }
}

具象クラスを継承する

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

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

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

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

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() { ... }
}

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

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() { ... }
}

抽象クラスを継承する

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

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
    • 頂いたコメントをもとに、『継承する際の共通点』を修正しました。
9
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?