Étendre la classe de données dans Kotlin

176

Les classes de données semblent remplacer les POJO à l'ancienne en Java. Il est tout à fait prévisible que ces classes permettent l'héritage, mais je ne vois aucun moyen pratique d'étendre une classe de données. Ce dont j'ai besoin est quelque chose comme ça:

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

Le code ci-dessus échoue en raison d'un conflit de component1()méthodes. Laisser une dataannotation dans une seule des classes ne fait pas non plus le travail.

Peut-être existe-t-il un autre langage pour étendre les classes de données?

UPD: Je pourrais annoter uniquement la classe enfant enfant, mais l' dataannotation ne gère que les propriétés déclarées dans le constructeur. Autrement dit, je devrais déclarer toutes les propriétés des parents openet les remplacer, ce qui est moche:

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
la source
3
Kotlin crée implicitement des méthodes componentN()qui retournent la valeur de la propriété N-ième. Voir la documentation sur les multi-déclarations
Dmitry
Pour ouvrir les propriétés, vous pouvez également créer une ressource abstraite ou utiliser le plugin du compilateur. Kotlin est strict sur le principe ouvert / fermé.
Željko Trogrlić
@Dmitry Puisque nous ne pouvions pas étendre une classe de données, est-ce que votre "solution" de garder la variable de classe parent ouverte et de simplement les remplacer dans la classe enfant serait un "ok"?
Archie G. Quiñones

Réponses:

164

La vérité est que les classes de données ne jouent pas très bien avec l'héritage. Nous envisageons d'interdire ou de restreindre sévèrement l'héritage des classes de données. Par exemple, on sait qu'il n'y a aucun moyen d'implémenter equals()correctement dans une hiérarchie sur des classes non abstraites.

Donc, tout ce que je peux offrir: n'utilisez pas l'héritage avec des classes de données.

Andrey Breslav
la source
Hey Andrey, comment fonctionne equals () tel qu'il est généré sur les classes de données maintenant? Cela correspond-il uniquement si le type est exact et que tous les champs communs sont égaux, ou seulement si les champs sont égaux? Il semble que, en raison de la valeur de l'héritage de classe pour approximer les types de données algébriques, il pourrait être utile de trouver une solution à ce problème. Fait intéressant, une recherche rapide a révélé cette discussion sur le sujet par Martin Odersky: artima.com/lejava/articles/equality.html
orospakr
3
Je ne pense pas qu'il y ait vraiment de solution à ce problème. Mon opinion jusqu'à présent est que les classes de données ne doivent pas du tout avoir de sous-classes de données.
Andrey Breslav
3
Et si nous avons un code de bibliothèque tel qu'un ORM et que nous voulons étendre son modèle pour avoir notre modèle de données persistant?
Krupal Shah
3
Les documents @AndreyBreslav sur les classes de données ne reflètent pas l'état après Kotlin 1.1. Comment les classes de données et l'héritage jouent-ils ensemble depuis la version 1.1?
Eugen Pechanec
2
@EugenPechanec Voir cet exemple: kotlinlang.org/docs/reference/…
Andrey Breslav
114

Déclarez les propriétés de la super-classe en dehors du constructeur comme abstraites et remplacez-les dans la sous-classe.

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ć
la source
15
cela semble être le plus flexible. Je souhaite sincèrement que nous puissions simplement avoir des classes de données héritées les unes des autres ...
Adam
Bonjour Monsieur, merci pour la manière soignée de gérer l'héritage de classe de données. Je rencontre un problème lorsque j'utilise la classe abstraite comme type générique. J'obtiens une Type Mismatcherreur: "T requis, trouvé: ressource". Pouvez-vous s'il vous plaît me dire comment peut-il être utilisé dans les génériques?
ashwin mahajan
Je voudrais également savoir si les génériques sont possibles dans les classes abstraites. Par exemple, que se passe-t-il si l'emplacement est une chaîne dans une classe de données héritée et une classe personnalisée (disons Location(long: Double, lat: Double))dans une autre?
Robbie Cronin
2
J'ai presque perdu espoir. Merci!
Michał Powłoka
La duplication des paramètres semble être une mauvaise manière d'implémenter l'héritage. Techniquement, puisque Book hérite de Resource, il doit savoir que l'identifiant et l'emplacement existent. Il ne devrait pas vraiment être nécessaire de les spécifier.
AndroidDev
23

La solution ci-dessus utilisant une classe abstraite génère en fait la classe correspondante et laisse la classe de données en étendre.

Si vous ne préférez pas la classe abstraite, que diriez-vous d'utiliser une interface ?

L'interface dans Kotlin peut avoir des propriétés comme indiqué dans cet article .

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

J'étais curieux de savoir comment Kotlin compilait cela. Voici le code Java équivalent (généré à l'aide de la fonctionnalité 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(), ...
}

Comme vous pouvez le voir, cela fonctionne exactement comme une classe de données normale!

Tura
la source
3
Malheureusement, l'implémentation du modèle d'interface pour une classe de données ne fonctionne pas avec l'architecture de Room.
Adam Hurwitz
@AdamHurwitz C'est dommage ... Je n'ai pas remarqué ça!
Tura le
4

@ La réponse de Željko Trogrlić est correcte. Mais nous devons répéter les mêmes champs que dans une classe abstraite.

De même, si nous avons des sous-classes abstraites à l'intérieur de la classe abstraite, alors dans une classe de données, nous ne pouvons pas étendre les champs de ces sous-classes abstraites. Nous devons d'abord créer une sous - classe de données , puis définir des champs.

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
la source
Nous pourrions déplacer History.Errors vers AbstractClass.Errors.Companion.SimpleErrors ou à l'extérieur et l'utiliser dans les classes de données plutôt que de le dupliquer à chaque classe de données héritée?
TWiStErRob
@TWiStErRob, heureux d'entendre une personne aussi célèbre! Je voulais dire que History.Errors peut changer dans chaque classe, de sorte que nous devrions le remplacer (par exemple, ajouter des champs).
CoolMind
4

Kotlin Traits peut vous aider.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

classes de données

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

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

utilisation de l'échantillon

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

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

Cette approche peut également être une solution de contournement pour les problèmes d'héritage avec @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
Jegan Babu
la source
2

Vous pouvez hériter d'une classe de données d'une classe sans données. L'héritage d'une classe de données d'une autre classe de données n'est pas autorisé car il n'existe aucun moyen de faire en sorte que les méthodes de classe de données générées par le compilateur fonctionnent de manière cohérente et intuitive en cas d'héritage.

Abraham Mathew
la source
1

Bien que la mise en œuvre equals()correctement dans une hiérarchie est en effet tout à fait un cornichon, il serait toujours agréable de soutenir héritant d' autres méthodes, par exemple: toString().

Pour être un peu plus concret, supposons que nous ayons la construction suivante (évidemment, cela ne fonctionne pas car elle toString()n'est pas héritée, mais ne serait-ce pas bien si c'était le cas?):

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)

En supposant que nos Useret Locationentités renvoient leurs ID de ressources appropriées ( UserResourceIdet LocationResourceIdrespectivement), appelant toString()à tout ResourceIdpourrait entraîner une très jolie petite représentation qui est généralement valable pour tous les sous - types: /users/4587, /locations/23, etc. Malheureusement, à cause non des sous - types hérité de redéfinie toString()méthode de la base abstraite ResourceId, appelant toString()fait des résultats dans une représentation moins jolie: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Il existe d'autres façons de modéliser ce qui précède, mais ces méthodes nous obligent soit à utiliser des classes sans données (manquant de nombreux avantages des classes de données), soit nous finissons par copier / répéter l' toString()implémentation dans toutes nos classes de données (pas d'héritage).

Khathuluu
la source
0

Vous pouvez hériter d'une classe de données d'une classe sans données.

Classe de base

open class BaseEntity (

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

classe enfant

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

Ça a marché.

tim4dev
la source
Sauf que maintenant vous ne pouvez pas définir les propriétés de nom et de description, et si vous les ajoutez au constructeur, la classe de données a besoin de val / var qui remplacera les propriétés de la classe de base.
Brill Pappin le