Estender a classe de dados no Kotlin

176

As classes de dados parecem substituir os POJOs à moda antiga em Java. É bastante esperado que essas classes permitam herança, mas não vejo uma maneira conveniente de estender uma classe de dados. O que eu preciso é algo como isto:

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

O código acima falha devido a conflito de component1()métodos. Deixar a dataanotação em apenas uma das classes também não funciona.

Talvez haja outro idioma para estender as classes de dados?

UPD: Eu posso anotar apenas classe filho filho, mas a dataanotação trata apenas das propriedades declaradas no construtor. Ou seja, eu teria que declarar todas as propriedades dos pais opene substituí-las, o que é feio:

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
fonte
3
Kotlin cria implicitamente métodos componentN()que retornam valor da N-ésima propriedade. Veja documentos sobre Multi-Declarações
Dmitry
Para abrir as propriedades, você também pode tornar o recurso abstrato ou usar o plug-in do compilador. Kotlin é rigoroso quanto ao princípio aberto / fechado.
Željko Trogrlić 6/11
@Dmitry Como não pudemos estender uma classe de dados, sua "solução" de manter a variável da classe pai aberta e simplesmente substituí-la na classe filho é uma solução "ok"?
Archie G. Quiñones

Respostas:

163

A verdade é: as classes de dados não funcionam muito bem com herança. Estamos pensando em proibir ou restringir severamente a herança de classes de dados. Por exemplo, sabe-se que não há como implementarequals() corretamente em uma hierarquia em classes não abstratas.

Então, tudo o que posso oferecer: não use herança com classes de dados.

Andrey Breslav
fonte
Hey Andrey, como funciona o equals () conforme é gerado nas classes de dados agora? Corresponde apenas se o tipo for exato e todos os campos comuns forem iguais ou apenas se os campos forem iguais? Parece que, devido ao valor da herança de classe para aproximar tipos de dados algébricos, pode valer a pena encontrar uma solução para esse problema. Curiosamente, uma pesquisa superficial revelou essa discussão sobre o tópico de Martin Odersky: artima.com/lejava/articles/equality.html
orospakr
3
Não acredito que exista muita solução para esse problema. Minha opinião até agora é que as classes de dados não devem ter subclasses de dados.
Andrey Breslav 30/03
3
e se tivermos um código de biblioteca como algum ORM e quisermos estender seu modelo para ter nosso modelo de dados persistente?
Krupal Shah
3
Os documentos @AndreyBreslav sobre classes de dados não refletem o estado após o Kotlin 1.1. Como as classes e a herança de dados funcionam juntas desde a versão 1.1?
Eugen Pechanec
2
@EugenPechanec Veja este exemplo: kotlinlang.org/docs/reference/…
Andrey
114

Declare propriedades na superclasse fora do construtor como abstratas e substitua-as na subclasse.

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ć
fonte
15
isso parece ser o mais flexível. Eu caro desejo que nós poderíamos apenas ter classes de dados herdar um do outro embora ...
Adam
Olá senhor, obrigado pela maneira elegante de lidar com a herança de classe de dados. Estou enfrentando um problema ao usar a classe abstrata como um tipo genérico. Eu recebo um Type Mismatcherro: "T necessário, encontrado: recurso". Você pode me dizer como ele pode ser usado em genéricos?
ashwin mahajan
Eu também gostaria de saber se os genéricos são possíveis nas classes abstratas. Por exemplo, e se localização é uma String em uma classe de dados herdada e uma classe personalizada (digamos Location(long: Double, lat: Double))em outro?
Robbie Cronin
2
Eu quase perdi minha esperança. Obrigado!
Michał Powłoka 18/04/19
Duplicar os parâmetros parece ser uma maneira ruim de implementar herança. Tecnicamente, como o Livro herda do Recurso, ele deve saber que o ID e o local existem. Realmente não deveria haver necessidade de especificá-las.
AndroidDev 30/04
23

A solução acima, usando a classe abstrata, na verdade gera a classe correspondente e permite que a classe de dados se estenda a partir dela.

Se você não prefere classe abstrata, que tal usar uma interface ?

A interface no Kotlin pode ter propriedades, como mostrado neste artigo .

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

Fiquei curioso para saber como Kotlin compila isso. Aqui está o código Java equivalente (gerado usando o recurso 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 você pode ver, ele funciona exatamente como uma classe de dados normal!

Tura
fonte
3
Infelizmente, a implementação do padrão de interface para uma classe de dados não funciona com a arquitetura do Room.
Adam Hurwitz
@ AdamHurwitz Isso é muito ruim ... eu não percebi isso!
Tura
4

@ Eljko Trogrlić resposta está correta. Mas temos que repetir os mesmos campos que em uma classe abstrata.

Além disso, se tivermos subclasses abstratas dentro da classe abstrata, em uma classe de dados não poderemos estender os campos dessas subclasses abstratas. Primeiro devemos criar subclasse de dados e depois 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()
}
CoolMind
fonte
Poderíamos mover History.Errors para AbstractClass.Errors.Companion.SimpleErrors ou outside e usá-lo nas classes de dados em vez de duplicá-lo em cada classe de dados herdada?
TWiStErRob 20/06/19
@TWiStErRob, feliz em ouvir uma pessoa tão famosa! Eu quis dizer que History.Errors pode mudar em todas as classes, para que possamos substituí-lo (por exemplo, adicionar campos).
CoolMind
4

Traços de Kotlin podem ajudar.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

classes de dados

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 amostra

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

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

Essa abordagem também pode ser uma solução alternativa para problemas de herança com o @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
fonte
2

Você pode herdar uma classe de dados de uma classe que não é de dados. A herança de uma classe de dados de outra classe de dados não é permitida porque não há como fazer com que os métodos de classe de dados gerados pelo compilador funcionem de maneira consistente e intuitiva em caso de herança.

Abraham Mathew
fonte
1

Embora a implementação equals()correta em uma hierarquia seja realmente um problema, ainda seria bom dar suporte à herança de outros métodos, por exemplo:toString() .

Para ser um pouco mais concreto, vamos assumir que temos a seguinte construção (obviamente, não funciona porque toString()não é herdada, mas não seria bom se funcionasse?):

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)

Supondo que o nosso Usere Locationentidades devolver os seus IDs de recursos apropriados ( UserResourceIde LocationResourceIdrespectivamente), chamando toString()em qualquer ResourceIdpoderia resultar em muito uma representação pouco agradável que é geralmente válido para todos os subtipos: /users/4587, /locations/23, etc. Infelizmente, porque não dos subtipos herdada para substituído toString()método do base abstrata ResourceId, chamando toString()realmente resulta em uma representação menos bonita: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Existem outras maneiras de modelar o exposto acima, mas elas nos forçam a usar não-classes de dados (perdendo muitos dos benefícios das classes de dados) ou acabamos copiando / repetindo a toString()implementação em todas as nossas classes de dados (sem herança).

Khathuluu
fonte
0

Você pode herdar uma classe de dados de uma classe que não é de dados.

Classe base

open class BaseEntity (

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

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

Funcionou.

tim4dev
fonte
Exceto que agora você não pode definir as propriedades de nome e descrição e, se adicioná-las ao construtor, a classe de dados precisa de val / var, que substituirá as propriedades da classe base.
Brill Pappin