Ampliar clase de datos en Kotlin


176

Las clases de datos parecen ser el reemplazo de los POJO anticuados en Java. Es bastante esperable que estas clases permitan la herencia, pero no veo una manera conveniente de extender una clase de datos. Lo que necesito es algo como esto:

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

El código anterior falla debido al choque de component1()métodos. Dejandodata anotaciones en solo una de las clases tampoco hace el trabajo.

¿Quizás hay otro idioma para extender las clases de datos?

UPD: podría anotar solo la clase secundaria hijo, pero la dataanotación solo maneja las propiedades declaradas en el constructor. Es decir, tendría que declarar todas las propiedades de los padres openy anularlas, lo cual es feo:

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 implícitamente métodos componentN()que devuelven el valor de la propiedad N-ésima. Ver documentos sobre Declaraciones Múltiples
Dmitry

Para abrir las propiedades, también puede hacer un resumen de recursos o usar el complemento del compilador. Kotlin es estricto con el principio abierto / cerrado.
Željko Trogrlić

@Dmitry Dado que no pudimos extender una clase de datos, ¿su "solución" de mantener abierta la variable de la clase primaria y simplemente anularlas en la clase secundaria es una solución "aceptable"?
Archie G. Quiñones

Respuestas:


164

La verdad es que las clases de datos no juegan demasiado bien con la herencia. Estamos considerando prohibir o restringir severamente la herencia de las clases de datos. Por ejemplo, se sabe que no hay forma de implementar equals()correctamente en una jerarquía en clases no abstractas.

Entonces, todo lo que puedo ofrecer: no use la herencia con clases de datos.


Hola Andrey, ¿cómo funciona equals () como se genera en las clases de datos ahora? ¿Solo coincide si el tipo es exacto y todos los campos comunes son iguales, o solo si los campos son iguales? Parece que, debido al valor de la herencia de clase para aproximar los tipos de datos algebraicos, podría valer la pena encontrar una solución a este problema. Curiosamente, una búsqueda superficial reveló esta discusión sobre el tema por Martin Odersky: artima.com/lejava/articles/equality.html
orospakr

3
No creo que haya mucha solución para este problema. Mi opinión hasta ahora es que las clases de datos no deben tener subclases de datos en absoluto.
Andrey Breslav

3
¿Qué pasa si tenemos un código de biblioteca como algunos ORM y queremos ampliar su modelo para tener nuestro modelo de datos persistentes?
Krupal Shah

3
@AndreyBreslav Los documentos sobre clases de datos no reflejan el estado después de Kotlin 1.1. ¿Cómo se combinan las clases de datos y la herencia desde 1.1?
Eugen Pechanec

2
@EugenPechanec Vea este ejemplo: kotlinlang.org/docs/reference/…
Andrey Breslav

114

Declare las propiedades en la superclase fuera del constructor como abstractas y anúlelas en la subclase.

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
Esto parece ser el más flexible. Sin embargo, desearía que pudiéramos tener clases de datos
Adam

Hola señor, gracias por la forma ordenada de manejar la herencia de clase de datos. Estoy enfrentando un problema cuando uso la clase abstracta como tipo genérico. Me sale un Type Mismatcherror: "T obligatorio, encontrado: Recurso". ¿Me puede decir cómo se puede usar en genéricos?
ashwin mahajan

También me gustaría saber si los genéricos son posibles en todas las clases abstractas. Por ejemplo, ¿qué pasa si la ubicación es una Cadena en una clase de datos heredados y una clase personalizada (digamos Location(long: Double, lat: Double))en otra?
Robbie Cronin

2
Casi pierdo la esperanza. ¡Gracias!
Michał Powłoka

Duplicar los parámetros parece ser una mala forma de implementar la herencia. Técnicamente, dado que Book hereda de Resource, debe saber que existen id y ubicación. Realmente no debería ser necesario especificarlos.
AndroidDev

23

La solución anterior que usa la clase abstracta en realidad genera la clase correspondiente y deja que la clase de datos se extienda desde ella.

Si no prefiere la clase abstracta, ¿qué tal usar una interfaz ?

La interfaz en Kotlin puede tener propiedades como se muestra en este artículo .

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

Tenía curiosidad por cómo Kotlin compila esto. Aquí hay un código Java equivalente (generado usando la función 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(), ...
}

Como puede ver, ¡funciona exactamente como una clase de datos normal!


3
Desafortunadamente, implementar el patrón de interfaz para una clase de datos no funciona con la arquitectura de Room.
Adam Hurwitz el

@AdamHurwitz Eso está muy mal ... ¡No me di cuenta de eso!
Tura

4

La respuesta de @ Željko Trogrlić es correcta. Pero tenemos que repetir los mismos campos que en una clase abstracta.

Además, si tenemos subclases abstractas dentro de la clase abstracta, entonces en una clase de datos no podemos extender los campos de estas subclases abstractas. Primero debemos crear una subclase de datos y luego definir campos.

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

¿Podríamos mover History.Errors a AbstractClass.Errors.Companion.SimpleErrors o afuera y usarlo en clases de datos en lugar de duplicarlo en cada clase de datos heredados?
TWiStErRob

@TWiStErRob, ¡me alegra escuchar a una persona tan famosa! Quise decir que History.Errors puede cambiar en cada clase, por lo que deberíamos anularlo (por ejemplo, agregar campos).
CoolMind

4

Los rasgos de Kotlin pueden ayudar.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

clases de datos

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

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

uso de muestra

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

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

Este enfoque también puede ser una solución para problemas de herencia 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

Puede heredar una clase de datos de una clase que no sea de datos. No se permite heredar una clase de datos de otra clase de datos porque no hay forma de hacer que los métodos de clase de datos generados por el compilador funcionen de manera coherente e intuitiva en caso de herencia.


1

Si bien la implementación equals()correcta en una jerarquía es bastante difícil, aún sería bueno admitir heredar otros métodos, por ejemplo:toString() .

Para ser un poco más concreto, supongamos que tenemos la siguiente construcción (obviamente, no funciona porque toString()no se hereda, pero ¿no sería bueno si lo hiciera?):

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)

Asumiendo nuestra Usery Locationentidades devolver su ID de los recursos apropiados ( UserResourceIdy LocationResourceIdrespectivamente), llamar toString()en cualquier ResourceIdpodría resultar en una representación muy poco agradable que es válido en general para todos los subtipos: /users/4587, /locations/23, etc. Por desgracia, porque no de los subtipos heredó a sobrescrito toString()el método de la base abstracta ResourceId, llamando toString()en realidad se traduce en una representación menos bonita: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Hay otras formas de modelar lo anterior, pero esas formas nos obligan a usar clases que no son de datos (perdiendo muchos de los beneficios de las clases de datos), o terminamos copiando / repitiendo la toString()implementación en todas nuestras clases de datos (sin herencia)


0

Puede heredar una clase de datos de una clase que no sea de datos.

Clase base

open class BaseEntity (

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

clase infantil

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

Funcionó.


Excepto que ahora no puede establecer las propiedades de nombre y descripción, y si las agrega al constructor, la clase de datos necesita val / var, que anulará las propiedades de la clase base.
Brill Pappin
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.