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


176

Классы данных, кажется, заменяют старомодные 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()

3
Котлин неявно создает методы, componentN()которые возвращают значение N-го свойства. Смотрите документы по мульти-декларациям
— Дмитрий

Для открытия свойств вы также можете сделать Resource абстрактным или использовать плагин компилятора. Котлин строго придерживается принципа «открыто / закрыто».
— Желько Трогрлич

@Dmitry Поскольку мы не можем расширить класс данных, может ли ваше «решение» оставить открытую переменную родительского класса и просто переопределить их в дочернем классе «ок»?
— Арчи Дж. Киньонес

Ответы:


164

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

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


Эй, Андрей, как работает метод equals (), который генерируется в классах данных? Совпадает ли оно только в том случае, если тип является точным и все общие поля равны или только если поля равны? Похоже, что из-за ценности наследования классов для аппроксимирующих алгебраических типов данных, возможно, стоит найти решение этой проблемы. Интересно, что беглый поиск выявил эту дискуссию по теме Мартина Одерского: artima.com/lejava/articles/equality.html
— orospakr

3
Я не верю, что есть много решений этой проблемы. Мое мнение таково, что у классов данных вообще не должно быть подклассов данных.
— Андрей Бреслав

3
Что, если у нас есть библиотечный код, такой как некоторый ORM, и мы хотим расширить его модель, чтобы иметь нашу постоянную модель данных?
— Крупал Шах

3
@AndreyBreslav Документы по классам данных не отражают состояние после Kotlin 1.1. Как классы данных и наследование играют вместе с версии 1.1?
— Евгений

2
@EugenPechanec Посмотрите этот пример: kotlinlang.org/docs/reference/…
— Андрей Бреслав,

114

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

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
это кажется наиболее гибким. Мне бы очень хотелось, чтобы мы могли просто иметь классы данных, наследуемые друг от друга, хотя ...
— Адам

Здравствуйте, сэр, спасибо за изящный способ обработки наследования классов данных. Я сталкиваюсь с проблемой, когда использую абстрактный класс как универсальный тип. Я получаю Type Mismatchошибку: «Требуется T, Найдено: Ресурс». Подскажите, пожалуйста, как его можно использовать в Generics?
— Ашвин Махаджан

Я также хотел бы знать, возможны ли дженерики для абстрактных классов. Например, что, если location - это строка в одном унаследованном классе данных и пользовательский класс (скажем, Location(long: Double, lat: Double))в другом?
— Робби Кронин,

2
Я почти потерял надежду. Спасибо!
— Михал Повлока

Дублирование параметров кажется плохим способом реализации наследования. Технически, поскольку Book наследуется от Resource, он должен знать, что идентификатор и местоположение существуют. Не должно быть необходимости указывать их.
— AndroidDev

23

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

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

Интерфейс в 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(), ...
}

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


3
К сожалению, реализация шаблона интерфейса для класса данных не работает с архитектурой Room.
— Адам Гурвиц

@AdamHurwitz Это очень плохо .. Я этого не заметил!
— Тура

4

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

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

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

Мы могли бы переместить History.Errors в AbstractClass.Errors.Companion.SimpleErrors или снаружи и использовать его в классах данных, а не дублировать его в каждом унаследованном классе данных?
— TWiStErRob

@TWiStErRob, рад слышать такого известного человека! Я имел в виду, что History.Errors может меняться в каждом классе, поэтому мы должны переопределить его (например, добавить поля).
— CoolMind

4

Черты Kotlin могут помочь.

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

2

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


1

При реализации 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()реализации во всех наших классах данных. (без наследства).


0

Вы можете наследовать класс данных от не-класса данных.

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

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

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


За исключением того, что теперь вы не можете устанавливать свойства name и description, и если вы добавляете их в конструктор, класс данных нуждается в val / var, который будет переопределять свойства базового класса.
— Брилл Паппин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.