Розширення класу даних у Котліні


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-го властивості. Дивіться документи про багатодекларації
Дмитро

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

@Dmitry Оскільки ми не могли розширити клас даних, чи буде ваше "рішення" зберігати змінну батьківського класу відкритою і просто переосмислити їх у дочірньому класі "нормально"?
Archie G. Quiñones

Відповіді:


163

Правда така: класи даних не надто добре грають із спадщиною. Ми розглядаємо можливість заборони або жорсткого обмеження успадкування класів даних. Наприклад, відомо, що немає можливості реалізовувати equals()правильно в ієрархії на неабразованих класах.

Отже, все, що я можу запропонувати: не використовуйте спадкування з класами даних.


Привіт, Андрію, як дорівнює (), як це створюється зараз на класах даних? Чи відповідає вона лише в тому випадку, якщо тип точний і всі загальні поля рівні, або лише якщо поля рівні? Схоже, через значення успадкування класів для наближення типів алгебраїчних даних, можливо, варто вирішити цю проблему. Цікаво, що побіжний пошук виявив це обговорення на тему Мартіна Одерського
orospakr

3
Я не вірю, що для вирішення цієї проблеми є багато. Наразі, на мою думку, класи даних взагалі не повинні мати підкласи даних.
Андрій Бреслав

3
що робити, якщо у нас є код бібліотеки, такий як деякі ORM, і ми хочемо розширити його модель, щоб мати нашу стійку модель даних?
Крупал Шах

3
@AndreyBreslav Документи щодо класів даних не відображають стан після Котліна 1.1. Як класи 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(long: Double, lat: Double))в іншому?
Роббі Кронін

2
Я майже втратив надію. Дякую!
Michał Powłoka

Дублювання параметрів здається поганим способом реалізації спадкування. Технічно, оскільки Книга успадковується від Resource, вона повинна знати, що id та місцезнаходження існують. Насправді не повинно бути необхідності їх вказувати.
AndroidDev

23

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

Якщо ви не віддаєте перевагу абстрактному класу, як щодо використання інтерфейсу ?

Інтерфейс у Котліні може мати властивості, як показано в цій статті .

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 [байт-код Котліна]):

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
На жаль, реалізація схеми інтерфейсу для класу даних не працює з архітектурою кімнати.
Адам Гурвіц

@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

Котлінські риси можуть допомогти.

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

Це спрацювало.


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