Rozszerz klasę danych w Kotlin

176

Wydaje się, że klasy danych zastępują staromodne POJO w Javie. Można się spodziewać, że te klasy pozwoliłyby na dziedziczenie, ale nie widzę wygodnego sposobu na rozszerzenie klasy danych. Potrzebuję czegoś takiego:

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

Powyższy kod nie działa z powodu kolizji component1()metod. Pozostawienie dataadnotacji tylko na jednej z klas też nie działa.

Być może istnieje inny idiom do rozszerzania klas danych?

UPD: Mogę datadodawać adnotacje tylko do klasy potomnej , ale adnotacja obsługuje tylko właściwości zadeklarowane w konstruktorze. Oznacza to, że musiałbym zadeklarować wszystkie właściwości rodzica openi zastąpić je, co jest brzydkie:

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()
Dmitry
źródło
3
Kotlin niejawnie tworzy metody, componentN()które zwracają wartość N-tej właściwości. Zobacz dokumentację dotyczącą wielu deklaracji
Dmitry,
Aby otworzyć właściwości, możesz również uczynić zasób abstrakcyjnym lub użyć wtyczki kompilatora. Kotlin ściśle przestrzega zasady open / closed.
Željko Trogrlić
@Dmitry Skoro nie mogliśmy rozszerzyć klasy danych, czy Twoje „rozwiązanie” polegające na utrzymywaniu otwartej zmiennej klasy nadrzędnej i zwykłym zastępowaniu ich w klasie podrzędnej byłoby rozwiązaniem „ok”?
Archie G. Quiñones

Odpowiedzi:

163

Prawda jest taka: klasy danych nie współpracują zbyt dobrze z dziedziczeniem. Rozważamy zakaz lub surowe ograniczenie dziedziczenia klas danych. Na przykład wiadomo, że nie ma sposobu, aby equals()poprawnie zaimplementować w hierarchii klas nieabstrakcyjnych.

A więc wszystko, co mogę zaoferować: nie używaj dziedziczenia z klasami danych.

Andrey Breslav
źródło
Hej Andrey, jak działa equals () generowane na klasach danych? Czy pasuje tylko wtedy, gdy typ jest dokładny, a wszystkie wspólne pola są równe, czy tylko wtedy, gdy pola są równe? Wydaje się, że ze względu na wartość dziedziczenia klas dla aproksymacji algebraicznych typów danych, warto byłoby znaleźć rozwiązanie tego problemu. Co ciekawe, pobieżne poszukiwania ujawniły dyskusję na ten temat autorstwa Martina Odersky'ego: artima.com/lejava/articles/equality.html
orospakr
3
Nie wierzę, że istnieje dobre rozwiązanie tego problemu. Jak dotąd uważam, że klasy danych nie mogą w ogóle mieć podklas danych.
Andrey Breslav
3
co, jeśli mamy kod biblioteki, taki jak jakiś ORM i chcemy rozszerzyć jego model o nasz trwały model danych?
Krupal Shah
3
@AndreyBreslav Docs on Data Classs nie odzwierciedlają stanu po Kotlin 1.1. Jak współdziałają klasy danych i dziedziczenie od wersji 1.1?
Eugen Pechanec
2
@EugenPechanec Zobacz ten przykład: kotlinlang.org/docs/reference/…
Andrey Breslav
114

Zadeklaruj właściwości w superklasie poza konstruktorem jako abstrakcyjne i zastąp je w podklasie.

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()
Željko Trogrlić
źródło
15
to wydaje się być najbardziej elastyczne. Naprawdę żałuję, że nie możemy po prostu dziedziczyć po sobie klas danych ...
Adam
Witam Panie, dziękuję za zgrabny sposób obsługi dziedziczenia klas danych. Mam problem, gdy używam klasy abstrakcyjnej jako typu ogólnego. Pojawia się Type Mismatchbłąd: „Wymagane T, znaleziono: zasób”. Czy możesz mi powiedzieć, jak można go używać w generycznych?
ashwin mahajan
Chciałbym również wiedzieć, czy typy generyczne są możliwe w klasach abstrakcyjnych. Na przykład co, jeśli lokalizacja jest ciągiem w jednej dziedziczonej klasie danych i klasie niestandardowej (powiedzmy Location(long: Double, lat: Double))w innej?
Robbie Cronin
2
Prawie straciłem nadzieję. Dzięki!
Michał Powłoka
Powielanie parametrów wydaje się być kiepskim sposobem implementacji dziedziczenia. Technicznie rzecz biorąc, ponieważ Book dziedziczy po Resource, powinna wiedzieć, że istnieje identyfikator i lokalizacja. Naprawdę nie powinno być potrzeby ich określania.
AndroidDev
23

Powyższe rozwiązanie wykorzystujące klasę abstrakcyjną faktycznie generuje odpowiednią klasę i pozwala klasie danych na jej rozszerzenie.

Jeśli nie wolisz klasy abstrakcyjnej, co powiesz na użycie interfejsu ?

Interfejs w Kotlin może mieć właściwości, jak pokazano w tym artykule .

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

Byłem ciekawy, jak Kotlin to kompiluje. Oto równoważny kod Java (wygenerowany przy użyciu funkcji Intellij [kod bajtowy Kotlin]):

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

Jak widać, działa dokładnie tak, jak zwykła klasa danych!

Tura
źródło
3
Niestety implementacja wzorca interfejsu dla klasy danych nie działa z architekturą Room.
Adam Hurwitz
@AdamHurwitz To szkoda ... Nie zauważyłem tego!
Tura
4

@ Željko Trogrlić odpowiedź jest prawidłowa. Ale musimy powtórzyć te same pola, co w klasie abstrakcyjnej.

Również jeśli mamy abstrakcyjne podklasy wewnątrz klasy abstrakcyjnej, to w klasie danych nie możemy rozszerzać pól z tych abstrakcyjnych podklas. Najpierw należy utworzyć podklasę danych, a następnie zdefiniować pola.

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
źródło
Moglibyśmy przenieść History.Errors do AbstractClass.Errors.Companion.SimpleErrors lub poza nią i użyć tego w klasach danych, zamiast powielać to w każdej dziedziczącej klasie danych?
TWiStErRob
@TWiStErRob, miło słyszeć tak sławną osobę! Miałem na myśli to, że History.Errors może się zmieniać w każdej klasie, więc powinniśmy to nadpisać (np. Dodać pola).
CoolMind
4

Kotlin Traits może pomóc.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

klasy danych

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

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

przykładowe użycie

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

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

To podejście może być również obejściem problemów z dziedziczeniem z @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
Jegan Babu
źródło
2

Możesz dziedziczyć klasę danych z klasy niebędącej danymi. Dziedziczenie klasy danych z innej klasy danych jest niedozwolone, ponieważ nie ma sposobu, aby metody klas danych generowanych przez kompilator działały spójnie i intuicyjnie w przypadku dziedziczenia.

Abraham Mathew
źródło
1

O ile equals()poprawne implementowanie w hierarchii jest rzeczywiście niezłe, dobrze byłoby wspierać dziedziczenie innych metod, na przykład:toString() .

Aby być bardziej konkretnym, załóżmy, że mamy następującą konstrukcję (oczywiście nie działa, ponieważ toString()nie jest dziedziczona, ale czy nie byłoby miło, gdyby tak było?):

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)

Zakładając, że nasze Useri Locationpodmioty wrócić do swoich odpowiednich identyfikatorów zasobów ( UserResourceIdi LocationResourceIdodpowiednio), dzwoniąc toString()na dowolny ResourceIdmoże spowodować dość ładnym małym reprezentacji, która jest ogólnie ważne dla wszystkich podtypów: /users/4587, /locations/23, itd. Niestety, z powodu non podtypów dziedziczona zastąpionej toString()metodzie z abstrakt baza ResourceId, nazywając toString()rzeczywiście skutkuje mniej ładna reprezentacji: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Istnieją inne sposoby modelowania powyższego, ale te sposoby albo zmuszają nas do korzystania z klas innych niż dane (tracą wiele zalet klas danych) lub w końcu kopiujemy / powtarzamy toString()implementację we wszystkich naszych klasach danych (bez dziedziczenia).

Khathuluu
źródło
0

Możesz dziedziczyć klasę danych z klasy niebędącej danymi.

Klasa podstawowa

open class BaseEntity (

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

klasa podrzędna

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

Zadziałało.

tim4dev
źródło
Tyle tylko, że teraz nie możesz ustawić właściwości nazwy i opisu, a jeśli dodasz je do konstruktora, klasa danych wymaga wartości val / var, która zastąpi właściwości klasy bazowej.
Brill Pappin