Расширить класс данных в Котлине

Классы данных, кажется, являются заменой старомодным POJO в Java. Вполне ожидаемо, что эти классы позволят наследовать, но я не вижу удобного способа расширить класс данных. Что мне нужно, это что-то вроде этого:

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

Приведенный выше код не работает из-за столкновения component1() методы. уход data аннотации только в одном из классов тоже не работают.

Возможно, есть другая идиома для расширения классов данных?

UPD: я мог бы аннотировать только дочерний дочерний класс, но data аннотация обрабатывает только свойства, объявленные в конструкторе. То есть я должен был бы объявить все родительские свойства open и переопределить их, что некрасиво

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

14 ответов

Решение

Правда в том, что классы данных не слишком хорошо играют с наследованием. Мы рассматриваем вопрос о запрете или строгом ограничении наследования классов данных. Например, известно, что нет способа реализовать equals() правильно в иерархии на неабстрактных классах.

Итак, все, что я могу предложить: не использовать наследование с классами данных.

Объявите свойства в суперклассе вне конструктора как абстрактные и переопределите их в подклассе.

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

Вышеупомянутое решение с использованием абстрактного класса фактически генерирует соответствующий класс, и пусть класс данных расширяется от него.

Если вы не предпочитаете абстрактный класс, как насчет использования интерфейса?

Интерфейс в Kotlin может иметь свойства, как показано в этой статье.

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

Мне было любопытно, как Котлин скомпилирует это. Вот эквивалентный Java-код (сгенерированный с использованием функции 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(), ...
}

Как видите, он работает точно так же, как обычный класс данных!

Kotlin Traits может помочь.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

классы данных

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

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

использование образца

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

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

Этот подход также может быть обходным путем для проблем наследования с @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

Вы можете унаследовать класс данных от класса, не относящегося к данным.

Базовый класс

open class BaseEntity (

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

дочерний класс

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

Это сработало.

@ Желько Трогрлич ответ правильный. Но мы должны повторить те же поля, что и в абстрактном классе.

Также, если у нас есть абстрактные подклассы внутри абстрактного класса, то в классе данных мы не можем расширять поля из этих абстрактных подклассов. Сначала мы должны создать подкласс данных, а затем определить поля.

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

Как я это сделал.

      open class ParentClass {
var var1 = false
var var2: String? = null
}

data class ChildClass(
    var var3: Long
) : ParentClass()

Работает нормально.

Вы можете наследовать класс данных от не-класса данных. Наследование класса данных от другого класса данных не допускается, потому что нет способа заставить сгенерированные компилятором методы класса данных работать согласованно и интуитивно в случае наследования.

Как обычно, когда наследование становится проблематичным, решением является композиция. См. «Предпочитать композицию наследованию?»..

Если вы просто хотите «расширить» свой класс несколькими дополнительными полями, для удобства вы можете использовать композицию вместе с некоторыми дополнительными геттерами:

      data class Book(
  val id: Long,
  val isbn: String,
  val author: String,
)

data class StoredBook(
  val book: Book,
  val version: Long,
  val createdAt: ZonedDateTime,
  val updatedAt: ZonedDateTime,
) {
  // proxy fields for convenience
  val id get() = book.id
  val isbn get() = book.isbn
  val author get() = book.author
}

Это делегирует свойства Bookbookэкземпляр, так что a можно использовать так же, какBookв большинстве случаев, но вы все равно можете обеспечить некоторую безопасность типов независимо от того, имеете ли вы дело с промежуточным состоянием Book или с постоянным состоянием.StoredBook.

Чтобы пойти еще дальше, вы можете создатьStoredResourceинтерфейс для любой сохраненной записи базы данных:

      interface StoredResource {
  val id: Long
  val version: Long
  val createdAt: ZonedDateTime
  val updatedAt: ZonedDateTime
}

data class Book(
  val id: Long,
  val isbn: String,
  val author: String,
)

data class StoredBook(
  val book: Book,
  override val version: Long,
  override val createdAt: ZonedDateTime,
  override val updatedAt: ZonedDateTime,
) : StoredResource {
  override val id get() = book.id
  val isbn get() = book.isbn
  val author get() = book.author
}

При реализации equals() правильно в иерархии действительно довольно солидно, все равно было бы неплохо поддерживать наследование других методов, например: toString().

Чтобы быть более конкретным, предположим, что у нас есть следующая конструкция (очевидно, она не работает, потому что toString() не наследуется, но было бы неплохо, если бы это было?):

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)

Если предположить, что наши User а также Location сущности возвращают свои соответствующие идентификаторы ресурсов (UserResourceId а также LocationResourceId соответственно), вызывая toString() на любом ResourceId может привести к довольно красивому небольшому представлению, которое обычно справедливо для всех подтипов: /users/4587, /locations/23и т.д. К сожалению, поскольку ни один из подтипов не унаследован для переопределения toString() метод из абстрактной базы ResourceId, звоню toString() на самом деле приводит к менее красивому представлению: <UserResourceId(id=UserId(value=4587))>, <LocationResourceId(id=LocationId(value=23))>

Есть и другие способы смоделировать вышеизложенное, но эти способы либо заставляют нас использовать классы, не относящиеся к данным (что упускает многие преимущества классов данных), либо мы в конечном итоге копируем / повторяем toString() реализация во всех наших классах данных (без наследования).

Другой способ реализовать наследование — использовать класс сabstractценности

      sealed class Parent {
    
    abstract val someVal: String
    
    data class Child1(override val someVal: String) : Parent()
    
    data class Child2(override val someVal: String) : Parent()
}

Класс данных, от которого я хотел наследоваться, не имел поведения, которое не следовало бы инкапсулировать в интерфейсе. С закрытым классом данных для «обычных» разработчиков интерфейса все объекты могут иметь преимуществаdataв то время как это похоже наUnitVectorрасширяетV.

      interface Vector {
    companion object {
        fun build(x : Float ...) : Vector = V(x ...)
        private data class V(override val x : Float ...) : Vector
    }
    val x : Float
    //functions, y etc. 
} 
data class UnitVector(override var x : Float ...) : Vector {
    init {
        //special behavior
    } 
}
      data class User(val id:Long, var name: String)
fun main() {
val user1 = User(id:1,name:"Kart")
val name = user1.name
println(name)
user1.name = "Michel"
val  user2 = User(id:1,name:"Michel")
println(user1 == user2)
println(user1)
val updateUser = user1.copy(name = "DK DK")
println(updateUser)
println(updateUser.component1())
println(updateUser.component2())
val (id,name) = updateUser
println("$id,$name") }

// вот результат ниже проверьте изображение, почему он показывает идентификатор ошибки:1 (компилятор говорит, что use = insted of double dot, где я вставляю значение)

Я обнаружил, что лучший способ использовать наследование в DTO - это создавать классы данных в java с помощью плагина Lombok .

Не забудьте указать для lombok.equalsAndHashCode.callSuper значение true в аннотации.

Другие вопросы по тегам