Kotlinでデータクラスを拡張する


176

データクラスは、Javaの昔ながらのPOJOに取って代わるようです。これらのクラスが継承を可能にすることはかなり期待できますが、データクラスを拡張する便利な方法はありません。私が必要なのはこのようなものです:

open data class Resource (var id: Long = 0, var location: String = "")
data class Book (var isbn: String) : Resource()

上記のコードは、component1()メソッドの衝突により失敗します。dataアノテーションを1つのクラスだけに残しても、機能しません。

おそらく、データクラスを拡張する別のイディオムがありますか?

UPD:子の子クラスdataのみに注釈を付ける場合がありますが、注釈はコンストラクタで宣言されたプロパティのみを処理します。つまり、すべての親のプロパティを宣言してopenオーバーライドする必要があります。

open class Resource (open var id: Long = 0, open var location: String = "")
data class Book (
    override var id: Long = 0,
    override var location: String = "",
    var isbn: String
) : Resource()

3
Kotlin componentN()は、N番目のプロパティの値を返すメソッドを暗黙的に作成します。マルチ宣言の
Dmitry

プロパティを開くには、Resourceを抽象化するか、コンパイラプラグインを使用することもできます。Kotlinは、オープン/クローズの原則に厳格です。
ゼリコTrogrlić

@Dmitryデータクラスを拡張できなかったので、親クラス変数を開いたままにして、子クラスでそれらを単にオーバーライドする「解決策」は「OK」の回避策になりますか?
Archie G.Quiñones19年

回答:


163

真実は次のとおりです。データクラスは継承ではあまりうまく機能しません。データクラスの継承を禁止または厳しく制限することを検討しています。たとえば、equals()非抽象クラスの階層に正しく実装する方法がないことがわかっています。

だから、私が提供できるすべてのこと:データクラスで継承を使用しないでください。


Andreyさん、データクラスで生成されたequals()はどのように機能するのでしょうか。タイプが正確で、すべての共通フィールドが等しい場合にのみ一致しますか、またはフィールドが等しい場合にのみ一致しますか?代数的データ型を近似するためのクラス継承の値のため、この問題の解決策を考え出す価値があるかもしれません。興味深いことに、大まかな
articles

3
この問題には多くの解決策があるとは思いません。これまでの私の意見では、データクラスにはデータサブクラスを含めないでください。
Andrey Breslav、2015年

3
ORMなどのライブラリコードがあり、そのモデルを拡張して永続データモデルを作成する場合はどうでしょうか。
Krupal Shah

3
@AndreyBreslav ドキュメントのデータクラスは、Kotlin 1.1以降の状態を反映していません。1.1以降、データクラスと継承はどのように連携しますか?
Eugen Pechanec 2017年

2
@EugenPechanecこの例を参照してください:kotlinlang.org/docs/reference/…–
Andrey

114

コンストラクターの外のスーパークラスでプロパティを抽象として宣言し、サブクラスでそれらをオーバーライドします。

abstract class Resource {
    abstract var id: Long
    abstract var location: String
}

data class Book (
    override var id: Long = 0,
    override var location: String = "",
    var isbn: String
) : Resource()

15
これが最も柔軟性があるようです。私は、データクラスが互いに継承し合えることを切に願っています...
Adam

こんにちは、データクラスの継承を適切に処理する方法に感謝します。抽象クラスをジェネリック型として使用すると問題が発生します。Type Mismatch「必要なT、見つかりました:リソース」というエラーが表示されます。ジェネリックでどのように使用できるか教えていただけますか?
アシュウィンマハジャン

ジェネリックが抽象クラス全体で可能かどうかも知りたいです。たとえば、場所が1つの継承されたデータクラスとカスタムクラスの文字列である場合(Location(long: Double, lat: Double))別のクラスで言いますか?)
ロビークロニン

2
私はほとんど希望を失いました。ありがとう!
のMichałPowłoka

パラメータを複製することは、継承を実装するための悪い方法のようです。技術的には、BookはResourceを継承しているため、IDと場所が存在することを認識しているはずです。実際にそれらを指定する必要はありません。
AndroidDev

23

抽象クラスを使用する上記のソリューションは、実際に対応するクラスを生成し、データクラスをそのクラスから拡張させます。

抽象クラスを好まない場合は、インターフェイスを使用してみませんか?

この記事に示すように、Kotlinのインターフェースはプロパティを持つことができます。

interface History {
    val date: LocalDateTime
    val name: String
    val value: Int
}

data class FixedHistory(override val date: LocalDateTime,
                        override val name: String,
                        override val value: Int,
                        val fixedEvent: String) : History

Kotlinがこれをコンパイルする方法に興味がありました。以下は、同等のJavaコードです(Intellij [Kotlinバイトコード]機能を使用して生成)。

public interface History {
   @NotNull
   LocalDateTime getDate();

   @NotNull
   String getName();

   int getValue();
}

public final class FixedHistory implements History {
   @NotNull
   private final LocalDateTime date;
   @NotNull
   private final String name;
   private int value;
   @NotNull
   private final String fixedEvent;

   // Boring getters/setters as usual..
   // copy(), toString(), equals(), hashCode(), ...
}

ご覧のとおり、通常のデータクラスとまったく同じように機能します。


3
残念ながら、データクラスのインターフェースパターンの実装は、Roomのアーキテクチャでは機能しません。
アダムフルヴィッツ2018

@AdamHurwitzそれは残念だ..私はそれに気づかなかった!
Tura

4

@ŽeljkoTrogrlić正解です。しかし、抽象クラスの場合と同じフィールドを繰り返す必要があります。

また、抽象クラス内に抽象サブクラスがある場合、データクラスでは、これらの抽象サブクラスからフィールドを拡張できません。最初にデータサブクラスを作成し、次にフィールドを定義する必要があります。

abstract class AbstractClass {
    abstract val code: Int
    abstract val url: String?
    abstract val errors: Errors?

    abstract class Errors {
        abstract val messages: List<String>?
    }
}



data class History(
    val data: String?,

    override val code: Int,
    override val url: String?,
    // Do not extend from AbstractClass.Errors here, but Kotlin allows it.
    override val errors: Errors?
) : AbstractClass() {

    // Extend a data class here, then you can use it for 'errors' field.
    data class Errors(
        override val messages: List<String>?
    ) : AbstractClass.Errors()
}

History.ErrorsをAbstractClass.Errors.Companion.SimpleErrorsまたは外部に移動して、継承する各データクラスで複製するのではなく、データクラスで使用することができますか?
TWiStErRob

@TWiStErRob、そのような有名人を聞いてうれしい!History.Errorsはすべてのクラスで変更できるため、オーバーライドする必要があります(たとえば、フィールドを追加する)。
CoolMind

4

Kotlinトレイトが役立ちます。

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

データクラス

data class Base(override val prop:String) : IBase

data class Derived(override val derived_prop:String,
                   private val base:IBase) :  IDerived, IBase by base

使用例

val b = Base("base")
val d = Derived("derived", b)

print(d.prop) //prints "base", accessing base class property
print(d.derived_prop) //prints "derived"

このアプローチは、@ Parcelizeの継承問題の回避策にもなります。

@Parcelize 
data class Base(override val prop:Any) : IBase, Parcelable

@Parcelize // works fine
data class Derived(override val derived_prop:Any,
                   private val base:IBase) : IBase by base, IDerived, Parcelable

2

非データクラスからデータクラスを継承できます。継承の場合、コンパイラーが生成したデータクラスメソッドを一貫して直感的に機能させる方法がないため、別のデータクラスからデータクラスを継承することはできません。


1

equals()階層に正しく実装することは確かに非常に厄介ですが、他のメソッドの継承をサポートすることは素晴らしいことですtoString()。たとえば:

もう少し具体的に言うと、次の構成があるとしましょう(明らかに、toString()は継承されていないため機能しませんが、継承してもいいのではないでしょうか)。

abstract class ResourceId(open val basePath: BasePath, open val id: Id) {

    // non of the subtypes inherit this... unfortunately...
    override fun toString(): String = "/${basePath.value}/${id.value}"
}
data class UserResourceId(override val id: UserId) : ResourceId(UserBasePath, id)
data class LocationResourceId(override val id: LocationId) : ResourceId(LocationBasePath, id)

私たちと仮定するUserと、Locationエンティティは(その適切なリソースIDを返すUserResourceIdLocationResourceId呼び出し、それぞれの)toString()任意の上ResourceId:すべてのサブタイプのため、一般的に有効であり、非常に素晴らしい小さな表現につながる可能性があり/users/4587/locations/23サブタイプの非がオーバーライドさに継承されたため、など残念なことにtoString()から方法抽象ベースResourceId、呼び出しはtoString()実際にはあまりきれいではない表現になります:<UserResourceId(id=UserId(value=4587))><LocationResourceId(id=LocationId(value=23))>

上記をモデル化する方法は他にもありますが、これらの方法では、データクラス以外のクラスを使用せざるを得ない(データクラスの多くの利点を逃している)かtoString()、すべてのデータクラスの実装をコピーまたは繰り返します(継承なし)。


0

非データクラスからデータクラスを継承できます。

基本クラス

open class BaseEntity (

@ColumnInfo(name = "name") var name: String? = null,
@ColumnInfo(name = "description") var description: String? = null,
// ...
)

子クラス

@Entity(tableName = "items", indices = [Index(value = ["item_id"])])
data class CustomEntity(

    @PrimaryKey
    @ColumnInfo(name = "id") var id: Long? = null,
    @ColumnInfo(name = "item_id") var itemId: Long = 0,
    @ColumnInfo(name = "item_color") var color: Int? = null

) : BaseEntity()

出来た。


ただし、名前と説明のプロパティを設定することはできません。これらをコンストラクタに追加すると、データクラスには、基本クラスのプロパティをオーバーライドするval / varが必要になります。
Brill Pappin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.