Datenklasse in Kotlin erweitern

176

Datenklassen scheinen der Ersatz für die altmodischen POJOs in Java zu sein. Es ist durchaus zu erwarten, dass diese Klassen eine Vererbung ermöglichen, aber ich sehe keine bequeme Möglichkeit, eine Datenklasse zu erweitern. Was ich brauche ist so etwas:

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

Der obige Code schlägt aufgrund von component1()Methodenkonflikten fehl . Das Belassen von dataAnmerkungen nur in einer der Klassen erledigt die Arbeit ebenfalls nicht.

Vielleicht gibt es eine andere Redewendung, um Datenklassen zu erweitern?

UPD: Ich kann nur untergeordnete untergeordnete Klassen mit dataAnmerkungen versehen, aber mit Anmerkungen werden nur Eigenschaften behandelt, die im Konstruktor deklariert sind. Das heißt, ich müsste alle Eigenschaften der Eltern deklarieren openund überschreiben, was hässlich ist:

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()
Dmitry
quelle
3
Kotlin erstellt implizit Methoden componentN(), die den Wert der N-ten Eigenschaft zurückgeben. Siehe Dokumente zu Multi-Deklarationen
Dmitry
Zum Öffnen der Eigenschaften können Sie Resource auch abstrakt machen oder das Compiler-Plugin verwenden. Kotlin ist streng in Bezug auf das Open / Closed-Prinzip.
Željko Trogrlić
@Dmitry Da wir eine Datenklasse nicht erweitern konnten, würde Ihre "Lösung", die übergeordnete Klassenvariable offen zu halten und sie einfach in der untergeordneten Klasse zu überschreiben, eine "OK" -Umgehung sein?
Archie G. Quiñones

Antworten:

163

Die Wahrheit ist: Datenklassen spielen mit Vererbung nicht so gut. Wir erwägen, die Vererbung von Datenklassen zu verbieten oder stark einzuschränken. Es ist beispielsweise bekannt, dass es keine Möglichkeit gibt, nicht equals()abstrakte Klassen in einer Hierarchie korrekt zu implementieren .

Alles, was ich anbieten kann: Verwenden Sie keine Vererbung mit Datenklassen.

Andrey Breslav
quelle
Hey Andrey, wie funktioniert equals (), wie es in Datenklassen generiert wird, jetzt? Stimmt es nur überein, wenn der Typ genau ist und alle gemeinsamen Felder gleich sind, oder nur, wenn die Felder gleich sind? Aufgrund des Werts der Klassenvererbung für die Approximation algebraischer Datentypen scheint es sinnvoll zu sein, eine Lösung für dieses Problem zu finden. Interessanterweise ergab eine flüchtige Suche diese Diskussion zu diesem Thema von Martin Odersky: artima.com/lejava/articles/equality.html
orospakr
3
Ich glaube nicht, dass es eine gute Lösung für dieses Problem gibt. Meine bisherige Meinung ist, dass Datenklassen überhaupt keine Datenunterklassen haben dürfen.
Andrey Breslav
3
Was ist, wenn wir einen Bibliothekscode wie ORM haben und dessen Modell um unser beständiges Datenmodell erweitern möchten?
Krupal Shah
3
@AndreyBreslav Docs auf Datenklassen spiegeln nicht den Zustand nach Kotlin 1.1. Wie spielen Datenklassen und Vererbung seit 1.1 zusammen?
Eugen Pechanec
2
@ EugenPechanec Siehe dieses Beispiel: kotlinlang.org/docs/reference/…
Andrey Breslav
114

Deklarieren Sie Eigenschaften in der Oberklasse außerhalb des Konstruktors als abstrakt und überschreiben Sie sie in der Unterklasse.

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()
Željko Trogrlić
quelle
15
Dies scheint am flexibelsten zu sein. Ich wünschte sehr, wir könnten nur Datenklassen voneinander erben lassen ...
Adam
Hallo Sir, vielen Dank für die saubere Handhabung der Datenklassenvererbung. Ich habe ein Problem, wenn ich die abstrakte Klasse als generischen Typ verwende. Ich erhalte die Type MismatchFehlermeldung "Erforderliches T, gefunden: Ressource". Können Sie mir bitte sagen, wie es in Generika verwendet werden kann?
Ashwin Mahajan
Ich würde auch gerne wissen, ob Generika über abstrakte Klassen hinweg möglich sind. Was ist zum Beispiel, wenn der Speicherort ein String in einer geerbten Datenklasse und eine benutzerdefinierte Klasse ist (sagen wir Location(long: Double, lat: Double))in einer anderen?
Robbie Cronin
2
Ich habe fast meine Hoffnung verloren. Vielen Dank!
Michał Powłoka
Das Duplizieren der Parameter scheint eine schlechte Methode zum Implementieren der Vererbung zu sein. Da Book von Resource erbt, sollte es wissen, dass ID und Speicherort vorhanden sind. Es sollte nicht wirklich notwendig sein, diese anzugeben.
AndroidDev
23

Die obige Lösung unter Verwendung einer abstrakten Klasse generiert tatsächlich eine entsprechende Klasse und lässt die Datenklasse daraus erweitern.

Wenn Sie keine abstrakte Klasse bevorzugen, wie wäre es dann mit einer Schnittstelle ?

Die Schnittstelle in Kotlin kann Eigenschaften haben, wie in diesem Artikel gezeigt .

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

Ich war gespannt, wie Kotlin das kompiliert. Hier ist äquivalenter Java-Code (generiert mit der Intellij-Funktion [Kotlin-Bytecode]):

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

Wie Sie sehen, funktioniert es genau wie eine normale Datenklasse!

Tura
quelle
3
Leider funktioniert die Implementierung des Schnittstellenmusters für eine Datenklasse nicht mit der Room-Architektur.
Adam Hurwitz
@AdamHurwitz Das ist schade .. das habe ich nicht bemerkt!
Tura
4

@ Željko Trogrlić Antwort ist richtig. Aber wir müssen die gleichen Felder wie in einer abstrakten Klasse wiederholen.

Auch wenn wir abstrakte Unterklassen innerhalb der abstrakten Klasse haben, können wir in einer Datenklasse keine Felder aus diesen abstrakten Unterklassen erweitern. Wir sollten zuerst eine Datenunterklasse erstellen und dann Felder definieren.

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()
}
CoolMind
quelle
Wir könnten History.Errors nach AbstractClass.Errors.Companion.SimpleErrors oder außerhalb verschieben und dies in Datenklassen verwenden, anstatt es bei jeder erbenden Datenklasse zu duplizieren?
TWiStErRob
@ TWiStErRob, froh, so eine berühmte Person zu hören! Ich meinte, dass sich History.Errors in jeder Klasse ändern können, so dass wir es überschreiben sollten (zum Beispiel Felder hinzufügen).
CoolMind
4

Kotlin Traits können helfen.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

Datenklassen

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

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

Beispielnutzung

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

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

Dieser Ansatz kann auch eine Problemumgehung für Vererbungsprobleme mit @Parcelize sein

@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
Jegan Babu
quelle
2

Sie können eine Datenklasse von einer Nicht-Datenklasse erben. Das Erben einer Datenklasse von einer anderen Datenklasse ist nicht zulässig, da vom Compiler generierte Datenklassenmethoden im Falle einer Vererbung nicht konsistent und intuitiv funktionieren können.

Abraham Mathew
quelle
1

Während die equals()korrekte Implementierung in einer Hierarchie in der Tat eine ziemliche Herausforderung darstellt, wäre es dennoch hilfreich, die Vererbung anderer Methoden zu unterstützen, zum Beispiel : toString().

Um etwas konkreter zu sein, nehmen wir an, wir haben das folgende Konstrukt (offensichtlich funktioniert es nicht, weil toString()es nicht vererbt wird, aber wäre es nicht schön, wenn es so wäre?):

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)

Unter der Annahme , unseren Userund LocationOrganisationen ihren entsprechenden Ressourcen - IDs zurückgeben ( UserResourceIdund LocationResourceIdjeweils), Aufruf toString()auf jedem ResourceIdin einer recht netten kleinen Darstellung führen könnten , die für alle Subtypen im Allgemeinen gilt: /users/4587, /locations/23usw. Leider, denn nicht die zu verdrängte geerbte Subtypen toString()Verfahren aus dem abstrakte Basis ResourceId, Aufruf führt toString()tatsächlich zu einer weniger hübschen Darstellung : <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Es gibt andere Möglichkeiten, das oben Genannte zu modellieren, aber diese Möglichkeiten zwingen uns entweder dazu, Nicht-Datenklassen zu verwenden (wobei viele Vorteile von Datenklassen fehlen), oder wir kopieren / wiederholen die toString()Implementierung in allen unseren Datenklassen (keine Vererbung).

Khathuluu
quelle
0

Sie können eine Datenklasse von einer Nicht-Datenklasse erben.

Basisklasse

open class BaseEntity (

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

Kinderklasse

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

Es funktionierte.

tim4dev
quelle
Abgesehen davon, dass Sie jetzt die Namen- und Beschreibungseigenschaften nicht festlegen können. Wenn Sie sie dem Konstruktor hinzufügen, benötigt die Datenklasse val / var, wodurch die Eigenschaften der Basisklasse überschrieben werden.
Brill Pappin