Harap pertimbangkan untuk mengubah struktur kode Anda sehingga nilai negatif diubah menjadi 0 saat kelas dibuat, dan bukan dalam getter. Jika Anda menimpa getter seperti yang dijelaskan dalam jawaban di bawah, semua metode lain yang dihasilkan seperti equals (), toString () dan akses komponen akan tetap menggunakan nilai negatif asli, yang kemungkinan akan menyebabkan perilaku yang mengejutkan.
yole
Jawaban:
148
Setelah menghabiskan hampir setahun penuh menulis Kotlin setiap hari, saya menemukan bahwa mencoba untuk mengganti kelas data seperti ini adalah praktik yang buruk. Ada 3 pendekatan yang valid untuk ini, dan setelah saya menyajikannya, saya akan menjelaskan mengapa pendekatan yang disarankan oleh jawaban lain itu buruk.
Miliki logika bisnis Anda yang membuat nilai data classperubahan menjadi 0 atau lebih besar sebelum memanggil konstruktor dengan nilai buruk. Ini mungkin pendekatan terbaik untuk kebanyakan kasus.
Jangan gunakan data class. Gunakan reguler classdan minta IDE Anda menghasilkan equalsdan hashCodemetode untuk Anda (atau tidak, jika Anda tidak membutuhkannya). Ya, Anda harus membuatnya kembali jika salah satu properti diubah pada objek, tetapi Anda memiliki kendali penuh atas objek tersebut.
classTest(value: Int) {
val value: Int = value
get() = if (field < 0) 0else field
overridefunequals(other: Any?): Boolean {
if (this === other) returntrueif (other !is Test) returnfalsereturntrue
}
overridefunhashCode(): Int {
return javaClass.hashCode()
}
}
Buat properti aman tambahan pada objek yang melakukan apa yang Anda inginkan alih-alih memiliki nilai pribadi yang diganti secara efektif.
dataclassTest(val value: Int) {
val safeValue: Intget() = if (value < 0) 0else value
}
Pendekatan buruk yang disarankan oleh jawaban lain:
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
Masalah dengan pendekatan ini adalah bahwa kelas data tidak benar-benar dimaksudkan untuk mengubah data seperti ini. Mereka benar-benar hanya untuk menyimpan data. Mengganti getter untuk kelas data seperti ini berarti itu Test(0)dan Test(-1)tidak akan equalsatu sama lain dan akan memiliki perbedaan hashCode, tetapi ketika Anda memanggil .value, mereka akan memiliki hasil yang sama. Ini tidak konsisten, dan meskipun mungkin berhasil untuk Anda, orang lain dalam tim Anda yang melihat ini adalah kelas data, mungkin secara tidak sengaja menyalahgunakannya tanpa menyadari bagaimana Anda telah mengubahnya / membuatnya tidak berfungsi seperti yang diharapkan (yaitu pendekatan ini tidak akan ' t bekerja dengan benar di a Mapatau a Set).
bagaimana dengan kelas data yang digunakan untuk serialisasi / deserialisation, meratakan struktur bersarang? Misal saya baru aja data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, dan saya anggap cukup bagus untuk kasus saya, tbh. Apa pendapat Anda tentang ini? (ada ofc bidang lain dan oleh karena itu saya percaya tidak masuk akal bagi saya untuk membuat ulang struktur json bersarang dalam kode saya)
Antek
@Antek Mengingat Anda tidak mengubah data, saya tidak melihat ada yang salah dengan pendekatan ini. Saya juga akan menyebutkan bahwa alasan Anda melakukan ini adalah karena model sisi server yang Anda kirim tidak nyaman digunakan pada klien. Untuk mengatasi situasi seperti itu, tim saya membuat model sisi klien yang kami terjemahkan model sisi servernya setelah deserialisasi. Kami membungkus semua ini dalam api sisi klien. Begitu Anda mulai mendapatkan contoh yang lebih rumit daripada yang Anda tunjukkan, pendekatan ini sangat membantu karena melindungi klien dari keputusan / apis model server yang buruk.
spierce7
Saya tidak setuju dengan apa yang Anda klaim sebagai "pendekatan terbaik". Masalah yang saya lihat adalah sangat umum ingin menetapkan nilai dalam kelas data, dan tidak pernah mengubahnya. Misalnya, mengurai string menjadi int. Getter / setter kustom pada kelas data tidak hanya berguna, tetapi juga diperlukan; jika tidak, Anda tertinggal dengan POJO kacang Jawa yang tidak melakukan apa-apa dan validasinya + perilakunya terkandung di beberapa kelas lain.
Abhijit Sarkar
Apa yang saya katakan adalah "Ini mungkin pendekatan terbaik untuk kebanyakan kasus". Dalam kebanyakan kasus, kecuali jika keadaan tertentu muncul, developer harus memiliki pemisahan yang jelas antara model dan algoritme / logika bisnisnya, di mana model yang dihasilkan dari algoritme tersebut dengan jelas mewakili berbagai status hasil yang mungkin. Kotlin sangat bagus untuk ini, dengan kelas tertutup, dan kelas data. Sebagai contoh parsing a string into an int, Anda dengan jelas mengizinkan logika bisnis penguraian dan penanganan kesalahan String non-numerik ke dalam kelas model Anda ...
spierce7
... Praktik mengeruhkan garis antara model dan logika bisnis selalu mengarah pada kode yang kurang dapat dipelihara, dan menurut saya adalah anti-pola. Mungkin 99% dari kelas data yang saya buat adalah penyetel yang tidak dapat diubah / kurang. Saya pikir Anda benar-benar menikmati meluangkan waktu untuk membaca tentang manfaat tim Anda menjaga modelnya tetap abadi. Dengan model yang tidak dapat diubah, saya dapat menjamin model saya tidak dimodifikasi secara tidak sengaja di beberapa tempat acak lainnya dalam kode, yang mengurangi efek samping, dan sekali lagi, mengarah ke kode yang dapat dipelihara. yaitu Kotlin tidak memisahkan Listdan MutableListtanpa alasan.
spierce7
31
Anda bisa mencoba sesuatu seperti ini:
dataclassTest(privateval _value: Int) {
val value = _value
get(): Int {
returnif (field < 0) 0else field
}
}
assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)
assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
Dalam kelas data Anda harus menandai parameter konstruktor utama dengan valatau var.
Saya menetapkan nilai _valueke valueuntuk menggunakan nama yang diinginkan untuk properti tersebut.
Saya mendefinisikan pengakses khusus untuk properti dengan logika yang Anda jelaskan.
Saya mendapat kesalahan pada IDE, dikatakan "Penginisialisasi tidak diizinkan di sini karena properti ini tidak memiliki bidang dukungan"
Cheng
6
Jawabannya tergantung pada kemampuan apa yang sebenarnya Anda gunakan yang datamenyediakan. @EPadron menyebutkan trik bagus (versi perbaikan):
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
Itu akan bekerja seperti yang diharapkan, ei itu memiliki satu bidang, satu pengambil, benar equals, hashcodedan component1. Tangkapannya begitu toStringdan copyaneh:
Untuk memperbaiki masalah dengan toStringAnda dapat mendefinisikan ulang dengan tangan. Saya tahu tidak ada cara untuk memperbaiki penamaan parameter tetapi tidak menggunakan datasama sekali.
Saya tahu ini adalah pertanyaan lama tetapi tampaknya tidak ada yang menyebutkan kemungkinan untuk menjadikan nilai pribadi dan menulis pengambil khusus seperti ini:
dataclassTest(privateval value: Int) {
fungetValue(): Int = if (value < 0) 0else value
}
Ini harus benar-benar valid karena Kotlin tidak akan menghasilkan pengambil default untuk bidang pribadi.
Tapi selain itu saya pasti setuju dengan spierce7 bahwa kelas data adalah untuk menyimpan data dan Anda harus menghindari logika "bisnis" hardcode di sana.
Saya setuju dengan solusi Anda tetapi daripada dalam kode Anda harus menyebutnya seperti ini val value = test.getValue() dan tidak seperti getter lainnya val value = test.value
gori
Iya. Itu benar. Ini sedikit berbeda jika Anda menyebutnya dari Java seperti biasanya.getValue()
bio007
1
Saya telah melihat jawaban Anda, saya setuju bahwa kelas data dimaksudkan untuk menyimpan data saja, tetapi terkadang kita perlu membuat sesuatu darinya.
Inilah yang saya lakukan dengan kelas data saya, saya mengubah beberapa properti dari val ke var, dan menimpanya di konstruktor.
seperti ini:
dataclassRecording(
val id: Int = 0,
val createdAt: Date = Date(),
val path: String,
val deleted: Boolean = false,
var fileName: String = "",
val duration: Int = 0,
var format: String = " "
) {
init {
if (fileName.isEmpty())
fileName = path.substring(path.lastIndexOf('\\'))
if (format.isEmpty())
format = path.substring(path.lastIndexOf('.'))
}
funasEntity(): rc {
return rc(id, createdAt, path, deleted, fileName, duration, format)
}
}
Membuat bidang bisa berubah sehingga Anda bisa mengubahnya selama inisialisasi adalah praktik yang buruk. Akan lebih baik untuk membuat konstruktor privat, dan kemudian membuat fungsi yang bertindak sebagai konstruktor (yaitu fun Recording(...): Recording { ... }). Mungkin juga kelas data bukanlah yang Anda inginkan, karena dengan kelas non-data Anda dapat memisahkan properti Anda dari parameter konstruktor. Lebih baik eksplisit dengan niat mutabilitas Anda dalam definisi kelas Anda. Jika bidang itu juga kebetulan bisa berubah, maka kelas data baik-baik saja, tetapi hampir semua kelas data saya tidak dapat diubah.
spierce7
@ spierce7 apakah seburuk itu pantas mendapatkan suara rendah? Bagaimanapun, solusi ini cocok untuk saya, tidak memerlukan banyak pengkodean dan itu membuat hash dan yang sama tetap utuh.
Simou
0
Ini tampaknya menjadi salah satu (di antara) kelemahan Kotlin yang mengganggu.
Tampaknya satu-satunya solusi yang masuk akal, yang sepenuhnya menjaga kompatibilitas kelas ke belakang adalah dengan mengubahnya menjadi kelas biasa (bukan kelas "data"), dan mengimplementasikan secara manual (dengan bantuan IDE) metode: hashCode ( ), sama dengan (), toString (), copy () dan componentN ()
classData3(i: Int)
{
var i: Int = i
overridefunequals(other: Any?): Boolean
{
if (this === other) returntrueif (other?.javaClass != javaClass) returnfalse
other as Data3
if (i != other.i) returnfalsereturntrue
}
overridefunhashCode(): Int
{
return i
}
overridefuntoString(): String
{
return"Data3(i=$i)"
}
funcomponent1():Int = i
funcopy(i: Int = this.i): Data3
{
return Data3(i)
}
}
Pertama, perhatikan bahwa _valueadalah var, tidakval , tapi di sisi lain, karena itu pribadi dan kelas data tidak dapat diwariskan dari, itu cukup mudah untuk memastikan bahwa itu tidak diubah dalam kelas.
Kedua, toString()menghasilkan hasil yang sedikit berbeda daripada jika _valuedinamai value, tetapi konsisten dan TestData(0).toString() == TestData(-1).toString().
Jawaban:
Setelah menghabiskan hampir setahun penuh menulis Kotlin setiap hari, saya menemukan bahwa mencoba untuk mengganti kelas data seperti ini adalah praktik yang buruk. Ada 3 pendekatan yang valid untuk ini, dan setelah saya menyajikannya, saya akan menjelaskan mengapa pendekatan yang disarankan oleh jawaban lain itu buruk.
Miliki logika bisnis Anda yang membuat nilai
data class
perubahan menjadi 0 atau lebih besar sebelum memanggil konstruktor dengan nilai buruk. Ini mungkin pendekatan terbaik untuk kebanyakan kasus.Jangan gunakan
data class
. Gunakan regulerclass
dan minta IDE Anda menghasilkanequals
danhashCode
metode untuk Anda (atau tidak, jika Anda tidak membutuhkannya). Ya, Anda harus membuatnya kembali jika salah satu properti diubah pada objek, tetapi Anda memiliki kendali penuh atas objek tersebut.class Test(value: Int) { val value: Int = value get() = if (field < 0) 0 else field override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Test) return false return true } override fun hashCode(): Int { return javaClass.hashCode() } }
Buat properti aman tambahan pada objek yang melakukan apa yang Anda inginkan alih-alih memiliki nilai pribadi yang diganti secara efektif.
data class Test(val value: Int) { val safeValue: Int get() = if (value < 0) 0 else value }
Pendekatan buruk yang disarankan oleh jawaban lain:
data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
Masalah dengan pendekatan ini adalah bahwa kelas data tidak benar-benar dimaksudkan untuk mengubah data seperti ini. Mereka benar-benar hanya untuk menyimpan data. Mengganti getter untuk kelas data seperti ini berarti itu
Test(0)
danTest(-1)
tidak akanequal
satu sama lain dan akan memiliki perbedaanhashCode
, tetapi ketika Anda memanggil.value
, mereka akan memiliki hasil yang sama. Ini tidak konsisten, dan meskipun mungkin berhasil untuk Anda, orang lain dalam tim Anda yang melihat ini adalah kelas data, mungkin secara tidak sengaja menyalahgunakannya tanpa menyadari bagaimana Anda telah mengubahnya / membuatnya tidak berfungsi seperti yang diharapkan (yaitu pendekatan ini tidak akan ' t bekerja dengan benar di aMap
atau aSet
).sumber
data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }
, dan saya anggap cukup bagus untuk kasus saya, tbh. Apa pendapat Anda tentang ini? (ada ofc bidang lain dan oleh karena itu saya percaya tidak masuk akal bagi saya untuk membuat ulang struktur json bersarang dalam kode saya)parsing a string into an int
, Anda dengan jelas mengizinkan logika bisnis penguraian dan penanganan kesalahan String non-numerik ke dalam kelas model Anda ...List
danMutableList
tanpa alasan.Anda bisa mencoba sesuatu seperti ini:
data class Test(private val _value: Int) { val value = _value get(): Int { return if (field < 0) 0 else field } } assert(1 == Test(1).value) assert(0 == Test(0).value) assert(0 == Test(-1).value) assert(1 == Test(1)._value) // Fail because _value is private assert(0 == Test(0)._value) // Fail because _value is private assert(0 == Test(-1)._value) // Fail because _value is private
Dalam kelas data Anda harus menandai parameter konstruktor utama dengan
val
atauvar
.Saya menetapkan nilai
_value
kevalue
untuk menggunakan nama yang diinginkan untuk properti tersebut.Saya mendefinisikan pengakses khusus untuk properti dengan logika yang Anda jelaskan.
sumber
Jawabannya tergantung pada kemampuan apa yang sebenarnya Anda gunakan yang
data
menyediakan. @EPadron menyebutkan trik bagus (versi perbaikan):data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
Itu akan bekerja seperti yang diharapkan, ei itu memiliki satu bidang, satu pengambil, benar
equals
,hashcode
dancomponent1
. Tangkapannya begitutoString
dancopy
aneh:println(Test(1)) // prints: Test(_value=1) Test(1).copy(_value = 5) // <- weird naming
Untuk memperbaiki masalah dengan
toString
Anda dapat mendefinisikan ulang dengan tangan. Saya tahu tidak ada cara untuk memperbaiki penamaan parameter tetapi tidak menggunakandata
sama sekali.sumber
Saya tahu ini adalah pertanyaan lama tetapi tampaknya tidak ada yang menyebutkan kemungkinan untuk menjadikan nilai pribadi dan menulis pengambil khusus seperti ini:
data class Test(private val value: Int) { fun getValue(): Int = if (value < 0) 0 else value }
Ini harus benar-benar valid karena Kotlin tidak akan menghasilkan pengambil default untuk bidang pribadi.
Tapi selain itu saya pasti setuju dengan spierce7 bahwa kelas data adalah untuk menyimpan data dan Anda harus menghindari logika "bisnis" hardcode di sana.
sumber
val value = test.getValue()
dan tidak seperti getter lainnyaval value = test.value
.getValue()
Saya telah melihat jawaban Anda, saya setuju bahwa kelas data dimaksudkan untuk menyimpan data saja, tetapi terkadang kita perlu membuat sesuatu darinya.
Inilah yang saya lakukan dengan kelas data saya, saya mengubah beberapa properti dari val ke var, dan menimpanya di konstruktor.
seperti ini:
data class Recording( val id: Int = 0, val createdAt: Date = Date(), val path: String, val deleted: Boolean = false, var fileName: String = "", val duration: Int = 0, var format: String = " " ) { init { if (fileName.isEmpty()) fileName = path.substring(path.lastIndexOf('\\')) if (format.isEmpty()) format = path.substring(path.lastIndexOf('.')) } fun asEntity(): rc { return rc(id, createdAt, path, deleted, fileName, duration, format) } }
sumber
fun Recording(...): Recording { ... }
). Mungkin juga kelas data bukanlah yang Anda inginkan, karena dengan kelas non-data Anda dapat memisahkan properti Anda dari parameter konstruktor. Lebih baik eksplisit dengan niat mutabilitas Anda dalam definisi kelas Anda. Jika bidang itu juga kebetulan bisa berubah, maka kelas data baik-baik saja, tetapi hampir semua kelas data saya tidak dapat diubah.Ini tampaknya menjadi salah satu (di antara) kelemahan Kotlin yang mengganggu.
Tampaknya satu-satunya solusi yang masuk akal, yang sepenuhnya menjaga kompatibilitas kelas ke belakang adalah dengan mengubahnya menjadi kelas biasa (bukan kelas "data"), dan mengimplementasikan secara manual (dengan bantuan IDE) metode: hashCode ( ), sama dengan (), toString (), copy () dan componentN ()
class Data3(i: Int) { var i: Int = i override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false other as Data3 if (i != other.i) return false return true } override fun hashCode(): Int { return i } override fun toString(): String { return "Data3(i=$i)" } fun component1():Int = i fun copy(i: Int = this.i): Data3 { return Data3(i) } }
sumber
Saya menemukan yang berikut ini menjadi pendekatan terbaik untuk mencapai apa yang Anda butuhkan tanpa putus
equals
danhashCode
:data class TestData(private var _value: Int) { init { _value = if (_value < 0) 0 else _value } val value: Int get() = _value } // Test value assert(1 == TestData(1).value) assert(0 == TestData(-1).value) assert(0 == TestData(0).value) // Test copy() assert(0 == TestData(-1).copy().value) assert(0 == TestData(1).copy(-1).value) assert(1 == TestData(-1).copy(1).value) // Test toString() assert("TestData(_value=1)" == TestData(1).toString()) assert("TestData(_value=0)" == TestData(-1).toString()) assert("TestData(_value=0)" == TestData(0).toString()) assert(TestData(0).toString() == TestData(-1).toString()) // Test equals assert(TestData(0) == TestData(-1)) assert(TestData(0) == TestData(-1).copy()) assert(TestData(0) == TestData(1).copy(-1)) assert(TestData(1) == TestData(-1).copy(1)) // Test hashCode() assert(TestData(0).hashCode() == TestData(-1).hashCode()) assert(TestData(1).hashCode() != TestData(-1).hashCode())
Namun,
Pertama, perhatikan bahwa
_value
adalahvar
, tidakval
, tapi di sisi lain, karena itu pribadi dan kelas data tidak dapat diwariskan dari, itu cukup mudah untuk memastikan bahwa itu tidak diubah dalam kelas.Kedua,
toString()
menghasilkan hasil yang sedikit berbeda daripada jika_value
dinamaivalue
, tetapi konsisten danTestData(0).toString() == TestData(-1).toString()
.sumber
_value
sedang dimodifikasi di blok initequals
danhashCode
tidak rusak.