Kotlin veri sınıfı için alıcıyı geçersiz kılma


99

Aşağıdaki Kotlin sınıfı verildiğinde:

data class Test(val value: Int)

IntDeğer negatifse 0 döndürmesi için alıcıyı nasıl geçersiz kılabilirim ?

Bu mümkün değilse, uygun bir sonuç elde etmek için bazı teknikler nelerdir?


14
Lütfen kodunuzun yapısını değiştirmeyi göz önünde bulundurun, böylece negatif değerler bir alıcıda değil, sınıf somutlaştırıldığında 0'a dönüştürülür. Alıcıyı aşağıdaki yanıtta açıklandığı gibi geçersiz kılarsanız, equals (), toString () ve bileşen erişimi gibi diğer tüm oluşturulan yöntemler yine de orijinal negatif değeri kullanır ve bu da muhtemelen şaşırtıcı davranışlara yol açar.
yole

Yanıtlar:


149

Neredeyse bir yıl boyunca Kotlin'i günlük yazmakla geçirdikten sonra, bunun gibi veri sınıflarını geçersiz kılmaya çalışmanın kötü bir uygulama olduğunu gördüm. Bunun için 3 geçerli yaklaşım var ve bunları sunduktan sonra, diğer yanıtların önerdiği yaklaşımın neden kötü olduğunu açıklayacağım.

  1. Yapıcıyı data classkötü değerle aramadan önce alter değerini 0 veya daha büyük olacak şekilde yaratan iş mantığınız olsun . Bu, çoğu durumda muhtemelen en iyi yaklaşımdır.

  2. Bir data class. Bir normal kullanın classve IDE'nizin sizin için equalsve hashCodeyöntemlerini oluşturmasını sağlayın (veya ihtiyacınız yoksa yapmayın). Evet, nesnedeki özelliklerden herhangi biri değiştirilirse, ancak nesnenin tam kontrolü size bırakılırsa onu yeniden oluşturmanız gerekir.

    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. Etkin bir şekilde geçersiz kılınan özel bir değere sahip olmak yerine, istediğiniz şeyi yapan nesne üzerinde ek bir güvenli özellik oluşturun.

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }
    

Diğer yanıtların önerdiği kötü bir yaklaşım:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

Bu yaklaşımla ilgili sorun, veri sınıflarının gerçekten bu gibi verileri değiştirmeye yönelik olmamasıdır. Gerçekten sadece veri tutmak içindir. Böyle bir veri sınıfı için toplama maddesinin geçersiz kılma anlamına gelir Test(0)ve Test(-1)olmaz equalbirbirlerini ve farklı olurdu hashCodes, ama beni aradığında .value, onlar da aynı sonuca ulaşılır. Bu tutarsızdır ve sizin için işe yarayabilirken, ekibinizdeki bunun bir veri sınıfı olduğunu gören diğer kişiler, onu nasıl değiştirdiğinizi / beklendiği gibi çalışmadığını fark etmeden yanlışlıkla onu kötüye kullanabilir (yani bu yaklaşım olmaz '' t a Mapveya a Set) ' da doğru çalışır .


Serileştirme / seriyi kaldırma, yuvalanmış bir yapıyı düzleştirme için kullanılan veri sınıfları ne olacak? Örneğin data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, az önce yazdım ve davam için oldukça iyi olduğunu düşünüyorum, tbh. Bunun hakkında ne düşünüyorsun? (orada diğer alanlarda ofc vardı ve dolayısıyla bu benim kodunda bu iç içe json yapısı oluşturulana kadar bana hiç mantıklı inanmak)
Antek

@Antek Verileri değiştirmediğiniz için, bu yaklaşımda yanlış bir şey görmüyorum. Bunu yapmanın nedeninin, gönderdiğiniz sunucu tarafı modelinin istemcide kullanımının uygun olmaması olduğunu da belirteceğim. Bu tür durumlara karşı koymak için ekibim, sunucu tarafı modelini serileştirmeden sonra çevirdiğimiz bir istemci tarafı modeli oluşturuyor. Tüm bunları bir istemci tarafı API ile tamamlıyoruz. Gösterdiğinizden daha karmaşık örnekler almaya başladığınızda, bu yaklaşım, istemciyi kötü sunucu modeli kararlarından / apilerden koruduğu için çok yararlıdır.
spierce7

"En iyi yaklaşım" olduğunu iddia ettiğiniz şeye katılmıyorum. Gördüğüm sorun şu ki , bir veri sınıfında bir değer ayarlamak ve onu asla değiştirmemek çok yaygın. Örneğin, bir dizeyi int olarak ayrıştırmak. Bir veri sınıfındaki özel alıcılar / ayarlayıcılar yalnızca yararlı değil, aynı zamanda gereklidir; aksi takdirde, hiçbir şey yapmayan Java bean POJO'larıyla kalırsınız ve davranışları + doğrulaması başka bir sınıfta bulunur.
Abhijit Sarkar

Söylediğim şey "Bu muhtemelen çoğu durumda en iyi yaklaşımdır". Çoğu durumda, belirli koşullar ortaya çıkmadıkça, geliştiriciler, modelleri ve algoritmalarından elde edilen modelin olası sonuçların çeşitli durumlarını açıkça temsil ettiği algoritma / iş mantığı arasında net bir ayrım yapmalıdır. Kotlin, mühürlü sınıflar ve veri sınıfları ile bunun için harika. Senin Örneğin parsing a string into an int, açıkça ayrıştırma ve hata modeli sınıfa sayısal olmayan dizeler taşıma iş mantığı izin veriyorsunuz ...
spierce7

... Model ve iş mantığı arasındaki çizgiyi bulandırma pratiği her zaman daha az sürdürülebilir koda yol açar ve bence bir anti-modeldir. Muhtemelen oluşturduğum veri sınıflarının% 99'u değişmez / eksik ayarlayıcılardır. Takımınızın modellerini değişmez tutmasının faydalarını okumak için biraz zaman ayırmaktan gerçekten keyif alacağınızı düşünüyorum. Değişmez modellerle, modellerimin yanlışlıkla kodda başka bir rastgele yerde değiştirilmediğini garanti edebilirim, bu da yan etkileri azaltır ve yine sürdürülebilir koda yol açar. yani Kotlin ayrılmadı Listve MutableListsebepsiz yere.
spierce7

31

Bunun gibi bir şey deneyebilirsin:

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
  • Bir veri sınıfında, birincil kurucunun parametrelerini valveya ile işaretlemeniz gerekir var.

  • Ben değerini tahsis edeceğim _valueiçin valueözellik için istediğiniz adı kullanmak için.

  • Tanımladığınız mantıkla özellik için özel bir erişimci tanımladım.


2
IDE'de bir hata aldım, "Bu özelliğin destek alanı olmadığı için burada başlatıcıya izin verilmiyor" yazıyor
Cheng

6

Cevap, gerçekte hangi yetenekleri kullandığınıza bağlıdır data. @EPadron şık bir numaradan bahsetti (geliştirilmiş versiyon):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

O irade beklendiği gibi ei sahip olduğu, çalışır bir sağ, alan, tek getter equals, hashcodeve component1. Yakalama şu toStringve copytuhaf:

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

toStringSizinle sorunu çözmek için elle yeniden tanımlayabilirsiniz. Parametre isimlendirmesini düzeltmenin bir yolunu bilmiyorum ama kullanmamam data.


2

Bunun eski bir soru olduğunu biliyorum ama görünen o ki kimse değeri özel yapma ve böyle özel alıcı yazma olasılığından bahsetmedi:

data class Test(private val value: Int) {
    fun getValue(): Int = if (value < 0) 0 else value
}

Kotlin özel alan için varsayılan alıcı oluşturmayacağından bu tamamen geçerli olmalıdır.

Ama aksi takdirde, veri sınıflarının veri tutmak için olduğu ve orada "iş" mantığını kodlamaktan kaçınmanız gerektiği konusunda spierce7'ye kesinlikle katılıyorum.


Çözümünüze katılıyorum, ancak koda göre onu bu şekilde adlandırmanız val value = test.getValue() ve diğer alıcılar gibi değil val value = test.value
gori

Evet. Bu doğru. Her zaman olduğu gibi Java'dan ararsanız biraz farklı.getValue()
bio007

1

Cevabınızı gördüm, veri sınıflarının yalnızca veri tutmak için olduğunu kabul ediyorum, ancak bazen bunlardan bir şeyler yapmamız gerekiyor.

İşte veri sınıfımla yaptığım şey, bazı özellikleri val'den var'a değiştirdim ve yapıcıda bunları geçersiz kıldım.

bunun gibi:

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

Alanları, başlatma sırasında değiştirebilmeniz için değiştirilebilir yapmak kötü bir uygulamadır. Yapıcıyı özel yapmak ve ardından bir kurucu (yani fun Recording(...): Recording { ... }) olarak işlev gören bir işlev oluşturmak daha iyi olur . Ayrıca veri sınıfı olmayan sınıflarla özelliklerinizi yapıcı parametrelerinizden ayırabileceğiniz için bir veri sınıfı da istediğiniz şey olmayabilir. Sınıf tanımınızda değişkenlik niyetleriniz konusunda açık olmak daha iyidir. Bu alanlar yine de değiştirilebilirse, o zaman bir veri sınıfı iyidir, ancak neredeyse tüm veri sınıflarım değişmezdir.
spierce7

@ spierce7 olumsuz bir oyu hak etmek gerçekten o kadar kötü mü? Her neyse, bu çözüm bana çok yakışıyor, çok fazla kodlama gerektirmiyor ve hash ve equals'ı sağlam tutuyor.
Simou

0

Bu, Kotlin'in (diğerlerinin yanı sıra) can sıkıcı dezavantajlarından biri gibi görünüyor.

Görünüşe göre, sınıfın geriye dönük uyumluluğunu tamamen koruyan tek makul çözüm, onu normal bir sınıfa ("veri" sınıfına değil) dönüştürmek ve elle (IDE'nin yardımıyla) şu yöntemleri uygulamaktır: hashCode ( ), equals (), toString (), copy () ve 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)
    }

}

1
Bunu bir dezavantaj olarak adlandıracağımdan emin değilim. Bu, Java'nın sunduğu bir özellik olmayan yalnızca veri sınıfı özelliğinin bir sınırlamasıdır.
spierce7

0

İhtiyaç duyduğunuz şeyi bozmadan elde etmek için en iyi yaklaşımın aşağıdakiler olduğunu buldum equalsve 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())

Ancak,

İlk olarak, not _valueolduğu vardeğil, valonu özel ve o oldukça kolay emin sınıfın içinde değiştirilmez emin olmak için var, veri sınıfları miras olamaz çünkü, ama diğer taraftan.

İkincisi, adlandırıldığından toString()biraz farklı bir sonuç üretir , ancak tutarlıdır ve ._valuevalueTestData(0).toString() == TestData(-1).toString()


@ spierce7 Hayır, değil. _valueinit bloğunda değiştirilmiş ve equalsve hashCode kırık değildir.
schatten

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.