Расширить класс данных в Котлине
Классы данных, кажется, являются заменой старомодным 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 в аннотации.