Ganti pengambil untuk kelas data Kotlin

99

Diberikan kelas Kotlin berikut:

data class Test(val value: Int)

Bagaimana cara menimpa Intgetter sehingga mengembalikan 0 jika nilainya negatif?

Jika ini tidak memungkinkan, teknik apa sajakah untuk mencapai hasil yang sesuai?

spierce7
sumber
14
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.

  1. 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.

  2. 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.

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

spierce7
sumber
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:

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 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.

EPadronU
sumber
2
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):

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, hashcodedan component1. Tangkapannya begitu toStringdan copyaneh:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

Untuk memperbaiki masalah dengan toStringAnda dapat mendefinisikan ulang dengan tangan. Saya tahu tidak ada cara untuk memperbaiki penamaan parameter tetapi tidak menggunakan datasama sekali.

voddan.dll
sumber
2

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.

bio007
sumber
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:

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)
    }
}
Simou
sumber
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 ()

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

}
Asher Stern
sumber
1
Tidak yakin saya akan menyebut ini sebagai kelemahan. Ini hanyalah batasan dari fitur kelas data, yang bukan merupakan fitur yang ditawarkan Java.
spierce7
0

Saya menemukan yang berikut ini menjadi pendekatan terbaik untuk mencapai apa yang Anda butuhkan tanpa putus equalsdan hashCode:

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

schatten
sumber
@ spierce7 Tidak, bukan. _valuesedang dimodifikasi di blok init equalsdan hashCode tidak rusak.
schatten
dapatkah Anda membantu saya dengan stackoverflow.com/questions/64015108/…
WISHY