Perpanjang kelas data di Kotlin

176

Kelas data tampaknya menjadi pengganti POJO kuno di Jawa. Sangat diharapkan bahwa kelas-kelas ini akan memungkinkan untuk warisan, tetapi saya tidak bisa melihat cara yang nyaman untuk memperluas kelas data. Yang saya butuhkan adalah sesuatu seperti ini:

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

Kode di atas gagal karena component1()metode bentrok . Meninggalkan dataanotasi hanya di satu kelas tidak akan berfungsi juga.

Mungkin ada idiom lain untuk memperpanjang kelas data?

UPD: Saya mungkin memberi anotasi hanya kelas anak anak, tetapi dataanotasi hanya menangani properti yang dideklarasikan di konstruktor. Yaitu, saya harus mendeklarasikan semua properti orangtua opendan menimpanya, yang jelek:

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
sumber
3
Kotlin secara implisit menciptakan metode componentN()yang mengembalikan nilai properti N-th. Lihat dokumen Multi-Deklarasi
Dmitry
Untuk membuka properti, Anda juga bisa membuat Sumberdaya abstrak atau menggunakan plugin kompiler. Kotlin ketat tentang prinsip terbuka / tertutup.
Željko Trogrlić
@Dmitry Karena kita tidak dapat memperluas kelas data, apakah "solusi" Anda untuk menjaga variabel kelas induk tetap terbuka dan hanya menimpanya di kelas anak sebagai "ok"?
Archie G. Quiñones

Jawaban:

163

Yang benar adalah: kelas data tidak bermain terlalu baik dengan warisan. Kami sedang mempertimbangkan untuk melarang atau sangat membatasi pewarisan kelas data. Misalnya, diketahui bahwa tidak ada cara untuk mengimplementasikan equals()dengan benar dalam hierarki pada kelas non-abstrak.

Jadi, yang bisa saya tawarkan: jangan gunakan warisan dengan kelas data.

Andrey Breslav
sumber
Hei Andrey, bagaimana equals () seperti yang dihasilkan pada kelas data berfungsi sekarang? Apakah hanya cocok jika jenisnya tepat dan semua bidang umum sama, atau hanya jika bidangnya sama? Sepertinya, karena nilai warisan kelas untuk mendekati tipe data aljabar, mungkin ada baiknya mencari solusi untuk masalah ini. Menariknya, pencarian sepintas mengungkapkan diskusi ini pada topik oleh Martin Odersky: artima.com/lejava/articles/equality.html
orospakr
3
Saya tidak percaya ada banyak solusi untuk masalah ini. Pendapat saya sejauh ini adalah bahwa kelas data tidak boleh memiliki data-subkelas sama sekali.
Andrey Breslav
3
bagaimana jika kita memiliki kode pustaka seperti beberapa ORM dan kami ingin memperluas modelnya untuk memiliki model data persisten kami?
Krupal Shah
3
@AndreyBreslav Docs pada kelas data tidak mencerminkan keadaan setelah Kotlin 1.1. Bagaimana kelas data dan warisan bermain bersama sejak 1.1?
Eugen Pechanec
2
@EugenPechanec Lihat contoh ini: kotlinlang.org/docs/reference/…
Andrey Breslav
114

Deklarasikan properti di super-class di luar konstruktor sebagai abstrak, dan timpa di sub-class.

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ć
sumber
15
ini tampaknya paling fleksibel. Saya sangat berharap kita bisa memiliki kelas data yang diwarisi dari satu sama lain meskipun ...
Adam
Halo Pak, terima kasih atas cara yang rapi dalam menangani Data Class Inheritance. Saya menghadapi masalah ketika saya menggunakan kelas abstrak sebagai Tipe Generik. Saya mendapatkan Type Mismatchkesalahan: "T yang Diperlukan, Ditemukan: Sumberdaya". Bisakah Anda memberi tahu saya bagaimana bisa digunakan dalam Generics?
ashwin mahajan
Saya juga ingin tahu apakah obat generik dimungkinkan di seluruh kelas abstrak. Misalnya, bagaimana jika lokasi adalah sebuah String dalam satu kelas data yang diwarisi dan kelas khusus (misalkan yang Location(long: Double, lat: Double))lain?
Robbie Cronin
2
Saya hampir kehilangan harapan. Terima kasih!
Michał Powłoka
Menduplikasi parameter tampaknya merupakan cara yang buruk untuk menerapkan warisan. Secara teknis, karena Buku mewarisi dari Sumber Daya, ia harus tahu bahwa id dan lokasi ada. Seharusnya tidak ada keharusan harus menentukan itu.
AndroidDev
23

Solusi di atas menggunakan kelas abstrak sebenarnya menghasilkan kelas yang sesuai dan membiarkan kelas data memanjang darinya.

Jika Anda tidak suka kelas abstrak, bagaimana dengan menggunakan antarmuka ?

Antarmuka di Kotlin dapat memiliki properti seperti yang ditunjukkan dalam artikel ini ..

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

Saya ingin tahu bagaimana Kotlin menyusun ini. Ini kode Java yang setara (dibuat menggunakan fitur 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(), ...
}

Seperti yang Anda lihat, ini berfungsi persis seperti kelas data normal!

Tura
sumber
3
Sayangnya menerapkan pola antarmuka untuk kelas data tidak berfungsi dengan arsitektur Room.
Adam Hurwitz
@ AdamHurwitz Sayang sekali .. Saya tidak menyadarinya!
Tura
4

@ Željko Trogrlić jawabannya benar. Tetapi kita harus mengulangi bidang yang sama seperti di kelas abstrak.

Juga jika kita memiliki subclass abstrak di dalam kelas abstrak, maka dalam kelas data kita tidak bisa memperluas bidang dari subclass abstrak ini. Pertama-tama kita harus membuat subkelas data dan kemudian mendefinisikan bidang.

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
sumber
Kita bisa memindahkan History.Errors ke AbstractClass.Errors.Companion.SimpleErrors atau di luar dan menggunakannya di kelas data daripada menduplikasinya di setiap kelas data yang diwariskan?
TWiStErRob
@TWiStErRob, senang mendengar orang yang begitu terkenal! Maksud saya History.Errors dapat berubah di setiap kelas, sehingga kita harus menimpanya (misalnya, menambahkan bidang).
CoolMind
4

Ciri Kotlin dapat membantu.

interface IBase {
    val prop:String
}

interface IDerived : IBase {
    val derived_prop:String
}

kelas data

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

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

penggunaan sampel

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

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

Pendekatan ini juga bisa menjadi solusi untuk masalah pewarisan dengan @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
sumber
2

Anda bisa mewarisi kelas data dari kelas non-data. Mewarisi kelas data dari kelas data lain tidak diperbolehkan karena tidak ada cara untuk membuat metode kelas data kompiler yang dihasilkan bekerja secara konsisten dan intuitif jika ada warisan.

Abraham Mathew
sumber
1

Sementara menerapkan equals()dengan benar dalam hirarki ini memang cukup acar, masih akan bagus untuk mendukung mewarisi metode lain, misalnya: toString().

Untuk menjadi sedikit lebih konkret, mari kita asumsikan kita memiliki konstruk berikut (jelas, itu tidak berhasil karena toString()tidak diwariskan, tetapi bukankah lebih baik jika itu?):

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)

Dengan asumsi kami Userdan Locationentitas kembali ID mereka sesuai sumber daya ( UserResourceIddan LocationResourceIdmasing-masing), menyerukan toString()pada setiap ResourceIddapat mengakibatkan cukup representasi kecil yang menyenangkan yang umumnya berlaku untuk semua subtipe: /users/4587, /locations/23, dll Sayangnya, karena non subtipe diwariskan kepada ditimpa toString()metode dari dasar abstrak ResourceId, memanggil toString()benar-benar menghasilkan representasi kurang cantik: <UserResourceId(id=UserId(value=4587))>,<LocationResourceId(id=LocationId(value=23))>

Ada beberapa cara lain untuk memodelkan hal di atas, tetapi cara-cara itu memaksa kita untuk menggunakan kelas-kelas non-data (kehilangan banyak manfaat dari kelas data), atau kita akhirnya menyalin / mengulangi toString()implementasi di semua kelas data kami (tidak ada warisan).

Khathuluu
sumber
0

Anda bisa mewarisi kelas data dari kelas non-data.

Kelas dasar

open class BaseEntity (

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

kelas anak

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

Itu berhasil.

tim4dev
sumber
Kecuali bahwa sekarang Anda tidak dapat mengatur properti nama dan deskripsi, dan jika Anda menambahkannya ke konstruktor, kelas data membutuhkan val / var yang akan menimpa properti kelas dasar.
Brill Pappin