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

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

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

Tura
источник
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()
}
CoolMind
источник
Мы могли бы переместить 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()реализации во всех наших классах данных. (без наследства).

Khathuluu
источник
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()

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

tim4dev
источник
За исключением того, что теперь вы не можете устанавливать свойства name и description, и если вы добавляете их в конструктор, класс данных нуждается в val / var, который будет переопределять свойства базового класса.
Брилл Паппин