Estendi la classe di dati in Kotlin


176

Le classi di dati sembrano sostituire i POJO vecchio stile in Java. È abbastanza prevedibile che queste classi consentirebbero l'ereditarietà, ma non vedo alcun modo conveniente per estendere una classe di dati. Ciò di cui ho bisogno è qualcosa del genere:

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

Il codice sopra non riesce a causa dello scontro di component1()metodi. Anche lasciare l' dataannotazione in una sola delle classi non funziona.

Forse esiste un altro linguaggio per estendere le classi di dati?

UPD: Potrei annotare solo la classe figlio secondaria, ma l' dataannotazione gestisce solo le proprietà dichiarate nel costruttore. Cioè, dovrei dichiarare tutte le proprietà dei genitori opene sostituirle, il che è brutto:

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 crea implicitamente metodi componentN()che restituiscono valore della proprietà N-esima. Vedi i documenti sulle dichiarazioni
multiple

Per aprire le proprietà, puoi anche rendere astratta la risorsa o usare il plugin del compilatore. Kotlin è severo riguardo al principio aperto / chiuso.
Željko Trogrlić,

@Dmitry Dal momento che non siamo riusciti a estendere una classe di dati, la tua "soluzione" di mantenere aperta la variabile della classe genitore e semplicemente sostituirla nella classe figlio sarebbe un "ok"?
Archie G. Quiñones,

Risposte:


163

La verità è che le classi di dati non giocano troppo bene con l'eredità. Stiamo pensando di vietare o limitare gravemente l'ereditarietà delle classi di dati. Ad esempio, è noto che non esiste alcun modo per implementare equals()correttamente in una gerarchia su classi non astratte.

Quindi, tutto quello che posso offrire: non usare l'ereditarietà con le classi di dati.


Ehi Andrey, come funziona ora uguale a () come viene generato nelle classi di dati? Corrisponde solo se il tipo è esatto e tutti i campi comuni sono uguali o solo se i campi sono uguali? Sembra che, a causa del valore dell'ereditarietà delle classi per l'approssimazione dei tipi di dati algebrici, potrebbe valere la pena trovare una soluzione a questo problema. È interessante notare che una ricerca superficiale ha rivelato questa discussione sull'argomento di Martin Odersky: artima.com/lejava/articles/equality.html
orospakr

3
Non credo che ci sia molta soluzione a questo problema. La mia opinione finora è che le classi di dati non debbano avere sottoclassi di dati.
Andrey Breslav,

3
cosa succede se abbiamo un codice di libreria come un ORM e vogliamo estendere il suo modello per avere il nostro modello di dati persistente?
Krupal Shah,

3
@AndreyBreslav Documenti su classi di dati non riflettono lo stato dopo Kotlin 1.1. In che modo le classi di dati e l'eredità giocano insieme dall'1.1?
Eugen Pechanec,

2
@EugenPechanec Vedi questo esempio: kotlinlang.org/docs/reference/…
Andrey Breslav

114

Dichiarare le proprietà in superclasse al di fuori del costruttore come astratte e sovrascriverle in sottoclasse.

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
questo sembra essere il più flessibile. Vorrei tanto che potessimo solo ereditare le classi di dati l'una dall'altra ...
Adam,

Ciao signore, grazie per il modo accurato di gestire l'ereditarietà delle classi di dati. Sto affrontando un problema quando uso la classe astratta come tipo generico. Viene visualizzato un Type Mismatcherrore: "Richiesto T, trovato: risorsa". Potete per favore dirmi come può essere utilizzato in Generics?
Ashwin Mahajan,

Vorrei anche sapere se i generici sono possibili attraverso classi astratte. Ad esempio, se la posizione è una stringa in una classe di dati ereditata e in una classe personalizzata (diciamo Location(long: Double, lat: Double))in un'altra?
Robbie Cronin

2
Ho quasi perso la speranza. Grazie!
Michał Powłoka,

La duplicazione dei parametri sembra essere un modo scadente di implementare l'ereditarietà. Tecnicamente, poiché Book eredita da Resource, dovrebbe sapere che id e posizione esistono. Non dovrebbe essere necessario specificare tali.
AndroidDev

23

La soluzione sopra che utilizza la classe astratta genera effettivamente la classe corrispondente e lascia che la classe di dati si estenda da essa.

Se non preferisci la classe astratta, che ne dici di usare un'interfaccia ?

L'interfaccia in Kotlin può avere proprietà come mostrato in questo articolo .

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

Ero curioso di sapere come Kotlin compila questo. Ecco un codice Java equivalente (generato usando la funzione Intellij [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(), ...
}

Come puoi vedere, funziona esattamente come una normale classe di dati!


3
Sfortunatamente l'implementazione del modello di interfaccia per una classe di dati non funziona con l'architettura di Room.
Adam Hurwitz,

@AdamHurwitz È un peccato .. Non me ne sono accorto!
Tura,

4

@ Željko Trogrlić la risposta è corretta. Ma dobbiamo ripetere gli stessi campi di una classe astratta.

Inoltre, se abbiamo sottoclassi astratte all'interno della classe astratta, in una classe di dati non possiamo estendere i campi da queste sottoclassi astratte. Dovremmo prima creare una sottoclasse di dati e quindi definire i campi.

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

Potremmo spostare History.Errors in AbstractClass.Errors.Companion.SimpleErrors o all'esterno e utilizzarlo in classi di dati anziché duplicarlo in ogni classe di dati ereditaria?
TWiStErRob,

@TWiStErRob, felice di sentire una persona così famosa! Intendevo che History.Errors può cambiare in ogni classe, quindi dovremmo sostituirlo (per esempio, aggiungere campi).
CoolMind,

4

I tratti di Kotlin possono aiutare.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

classi di dati

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

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

utilizzo del campione

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

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

Questo approccio può anche essere una soluzione per problemi di ereditarietà con @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

È possibile ereditare una classe di dati da una classe non di dati. L'ereditarietà di una classe di dati da un'altra classe di dati non è consentita perché non è possibile far funzionare i metodi della classe di dati generati dal compilatore in modo coerente e intuitivo in caso di ereditarietà.


1

Mentre l'attuazione equals()in modo corretto in una gerarchia è davvero un bel salamoia, sarebbe comunque bello per sostenere ereditare altri metodi, ad esempio: toString().

Per essere un po 'più concreti, supponiamo di avere il seguente costrutto (ovviamente, non funziona perché toString()non è ereditato, ma non sarebbe bello se lo facesse?):

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)

Supponendo che la nostra Usere Locationle entità ritornano loro ID di risorse adeguate ( UserResourceIde LocationResourceIdrispettivamente), chiamando toString()in qualsiasi ResourceIdpotrebbe tradursi in un bel po 'di rappresentanza bello che è generalmente valida per tutti i sottotipi: /users/4587, /locations/23, ecc Purtroppo, a causa non dei sottotipi ereditato a override toString()metodo dal base astratta ResourceId, chiamando toString()in realtà si traduce in una meno bella di rappresentanza: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Esistono altri modi per modellare quanto sopra, ma questi modi ci costringono a usare classi non di dati (perdendo molti dei vantaggi delle classi di dati) o finiamo per copiare / ripetere l' toString()implementazione in tutte le nostre classi di dati (nessuna eredità).


0

È possibile ereditare una classe di dati da una classe non di dati.

Classe base

open class BaseEntity (

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

classe per bambini

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

Ha funzionato.


Tranne il fatto che ora non è possibile impostare le proprietà del nome e della descrizione e se le si aggiungono al costruttore, la classe di dati necessita di val / var che sostituirà le proprietà della classe di base.
Brill Pappin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.