Getter für Kotlin-Datenklasse überschreiben

93

Angesichts der folgenden Kotlin-Klasse:

data class Test(val value: Int)

Wie würde ich den IntGetter überschreiben, damit er 0 zurückgibt, wenn der Wert negativ ist?

Wenn dies nicht möglich ist, mit welchen Techniken kann ein geeignetes Ergebnis erzielt werden?

spierce7
quelle
13
Bitte erwägen Sie, die Struktur Ihres Codes so zu ändern, dass negative Werte bei der Instanziierung der Klasse in 0 konvertiert werden und nicht in einem Getter. Wenn Sie den Getter wie in der folgenden Antwort beschrieben überschreiben, verwenden alle anderen generierten Methoden wie equals (), toString () und Komponentenzugriff weiterhin den ursprünglichen negativen Wert, was wahrscheinlich zu überraschendem Verhalten führt.
Yole

Antworten:

137

Nachdem ich fast ein ganzes Jahr lang täglich Kotlin geschrieben habe, habe ich festgestellt, dass der Versuch, Datenklassen wie diese zu überschreiben, eine schlechte Praxis ist. Hierfür gibt es drei gültige Ansätze. Nachdem ich sie vorgestellt habe, erkläre ich, warum der Ansatz, den andere Antworten vorgeschlagen haben, schlecht ist.

  1. Lassen Sie Ihre Geschäftslogik, die data classden Wert erstellt , den Wert auf 0 oder höher ändern, bevor Sie den Konstruktor mit dem falschen Wert aufrufen. Dies ist wahrscheinlich der beste Ansatz für die meisten Fälle.

  2. Verwenden Sie keine data class. Verwenden Sie eine reguläre Methode und lassen Sie classIhre IDE die Methoden equalsund hashCodefür Sie generieren (oder nicht, wenn Sie sie nicht benötigen). Ja, Sie müssen es neu generieren, wenn eine der Eigenschaften des Objekts geändert wird, aber Sie haben die vollständige Kontrolle über das Objekt.

    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. Erstellen Sie eine zusätzliche sichere Eigenschaft für das Objekt, die das tut, was Sie möchten, anstatt einen privaten Wert zu haben, der effektiv überschrieben wird.

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

Ein schlechter Ansatz, den andere Antworten vorschlagen:

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

Das Problem bei diesem Ansatz ist, dass Datenklassen nicht wirklich dazu gedacht sind, solche Daten zu ändern. Sie dienen wirklich nur zum Speichern von Daten. Das Überschreiben des Getters für eine Datenklasse wie diese würde dies bedeuten Test(0)und Test(-1)würde sich nicht equalgegenseitig beeinflussen und unterschiedliche hashCodes haben, aber wenn Sie anrufen .value, würden sie das gleiche Ergebnis haben. Dies ist inkonsistent, und obwohl es für Sie möglicherweise funktioniert, können andere Personen in Ihrem Team, die dies als Datenklasse ansehen, es versehentlich missbrauchen, ohne zu bemerken, wie Sie es geändert haben / es nicht wie erwartet funktionieren lassen (dh dieser Ansatz würde nicht funktionieren). in a Mapoder a Set) nicht richtig funktionieren .

spierce7
quelle
Was ist mit Datenklassen, die für die Serialisierung / Deserialisierung verwendet werden und eine verschachtelte Struktur reduzieren? ZB habe ich gerade geschrieben data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, und ich halte es für ziemlich gut für meinen Fall, tbh. Was denkst du darüber? (Es gab viele andere Felder und daher glaube ich, dass es für mich keinen Sinn machte, diese verschachtelte JSON-Struktur in meinem Code neu zu erstellen.)
Antek
@Antek Da Sie die Daten nicht ändern, sehe ich an diesem Ansatz nichts Falsches. Ich werde auch erwähnen, dass der Grund, warum Sie dies tun, darin besteht, dass das serverseitige Modell, das Sie gesendet werden, auf dem Client nicht bequem zu verwenden ist. Um solchen Situationen entgegenzuwirken, erstellt mein Team ein clientseitiges Modell, in das wir das serverseitige Modell nach der Deserialisierung übersetzen. Wir packen das alles in eine clientseitige API. Sobald Sie Beispiele erhalten, die komplizierter sind als das, was Sie gezeigt haben, ist dieser Ansatz sehr hilfreich, da er den Client vor schlechten Servermodellentscheidungen / APIs schützt.
spierce7
Ich bin nicht einverstanden mit dem, was Sie für den "besten Ansatz" halten. Das Problem, das ich sehe, ist, dass es sehr häufig vorkommt, dass ein Wert in einer Datenklasse festgelegt und niemals geändert werden soll. Beispiel: Parsen eines Strings in ein int. Benutzerdefinierte Getter / Setter für eine Datenklasse sind nicht nur nützlich, sondern auch erforderlich. Andernfalls bleiben Java-Bean-POJOs übrig, die nichts tun, und ihr Verhalten und ihre Validierung sind in einer anderen Klasse enthalten.
Abhijit Sarkar
Was ich sagte ist "Dies ist wahrscheinlich der beste Ansatz für die meisten Fälle". In den meisten Fällen sollten Entwickler, sofern keine bestimmten Umstände eintreten, eine klare Trennung zwischen ihrem Modell und dem Algorithmus / der Geschäftslogik aufweisen, wobei das resultierende Modell aus ihrem Algorithmus die verschiedenen Zustände der möglichen Ergebnisse klar darstellt. Kotlin ist dafür fantastisch, mit versiegelten Klassen und Datenklassen. Für Ihr Beispiel von parsing a string into an interlauben Sie eindeutig die Geschäftslogik des Parsens und der Fehlerbehandlung nicht numerischer Zeichenfolgen in Ihrer Modellklasse ...
spierce7
... Die Praxis, die Grenze zwischen Modell und Geschäftslogik zu verwischen, führt immer zu weniger wartbarem Code, und ich würde behaupten, dass dies ein Anti-Muster ist. Wahrscheinlich sind 99% der von mir erstellten Datenklassen unveränderlich / ohne Setter. Ich denke, Sie würden sich wirklich gerne etwas Zeit nehmen, um über die Vorteile Ihres Teams zu lesen, das seine Modelle unveränderlich hält. Mit unveränderlichen Modellen kann ich garantieren, dass meine Modelle nicht versehentlich an einer anderen zufälligen Stelle im Code geändert werden, was die Nebenwirkungen verringert und wiederum zu wartbarem Code führt. dh Kotlin trennte sich nicht Listund MutableListohne Grund.
spierce7
31

Sie könnten so etwas versuchen:

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
  • In einer Datenklasse müssen Sie die Parameter des primären Konstruktors mit entweder valoder markieren var.

  • Ich weise den Wert von _valuezu valuezu, um den gewünschten Namen für die Eigenschaft zu verwenden.

  • Ich habe einen benutzerdefinierten Accessor für die Eigenschaft mit der von Ihnen beschriebenen Logik definiert.

EPadronU
quelle
1
Ich habe einen Fehler in der IDE erhalten, der besagt, dass "Initializer hier nicht zulässig ist, da diese Eigenschaft kein Hintergrundfeld hat"
Cheng
6

Die Antwort hängt davon ab, welche Funktionen Sie tatsächlich verwenden data. @ EPadron erwähnte einen raffinierten Trick (verbesserte Version):

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

Das wird wie erwartet funktionieren, ei es hat ein Feld, einen Getter, richtig equals, hashcodeund component1. Der Haken ist das toStringund copysind komisch:

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

Um das Problem zu beheben toString, können Sie es von Hand neu definieren. Ich kenne keine Möglichkeit, die Parameternamen zu korrigieren, aber überhaupt nicht zu verwenden data.

Voddan
quelle
2

Ich weiß, dass dies eine alte Frage ist, aber es scheint, dass niemand die Möglichkeit erwähnt hat, Wert privat zu machen und einen benutzerdefinierten Getter wie diesen zu schreiben:

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

Dies sollte vollkommen gültig sein, da Kotlin keinen Standard-Getter für private Felder generiert.

Ansonsten stimme ich spierce7 definitiv zu, dass Datenklassen zum Speichern von Daten dienen und Sie es vermeiden sollten, die "Geschäfts" -Logik dort fest zu codieren.

bio007
quelle
Ich bin mit Ihrer Lösung einverstanden, aber als im Code müssten Sie es so nennen val value = test.getValue() und nicht wie andere Getter val value = test.value
gori
Ja. Das ist richtig. Es ist ein bisschen anders, wenn man es von Java aus nennt, da es immer da ist.getValue()
bio007
1

Ich habe Ihre Antwort gesehen, ich stimme zu, dass Datenklassen nur zum Speichern von Daten gedacht sind, aber manchmal müssen wir etwas daraus machen.

Folgendes mache ich mit meiner Datenklasse: Ich habe einige Eigenschaften von val in var geändert und sie im Konstruktor überschrieben.

wie so:

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
quelle
Es ist eine schlechte Praxis, Felder veränderbar zu machen, damit Sie sie während der Initialisierung ändern können. Es ist besser, den Konstruktor privat zu machen und dann eine Funktion zu erstellen, die als Konstruktor fungiert (dh fun Recording(...): Recording { ... }). Vielleicht ist eine Datenklasse auch nicht das, was Sie wollen, da Sie mit Nicht-Datenklassen Ihre Eigenschaften von Ihren Konstruktorparametern trennen können. Es ist besser, Ihre Veränderungsabsichten in Ihrer Klassendefinition explizit anzugeben. Wenn diese Felder sowieso auch veränderbar sind, ist eine Datenklasse in Ordnung, aber fast alle meine Datenklassen sind unveränderlich.
spierce7
@ spierce7 ist es wirklich so schlimm, eine Abwertung zu verdienen? Wie auch immer, diese Lösung passt gut zu mir, sie erfordert nicht so viel Codierung und sie hält den Hash und die Gleichheit intakt.
Simou
0

Dies scheint (unter anderem) ein ärgerlicher Nachteil von Kotlin zu sein.

Es scheint, dass die einzig vernünftige Lösung, die die Abwärtskompatibilität der Klasse vollständig gewährleistet, darin besteht, sie in eine reguläre Klasse (keine "Daten" -Klasse) zu konvertieren und die folgenden Methoden (mit Hilfe der IDE) manuell zu implementieren: hashCode ( ), equals (), toString (), copy () und 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
quelle
Ich bin mir nicht sicher, ob ich das einen Nachteil nennen würde. Dies ist lediglich eine Einschränkung der Datenklassenfunktion, die Java nicht bietet.
spierce7
0

Ich fand, dass der folgende Ansatz der beste ist, um das zu erreichen, was Sie brauchen, ohne zu brechen equalsund 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())

Jedoch,

Zuerst beachten Sie, dass _valueist var, nicht val, aber auf der anderen Seite, da es nicht privat und Datenklassen ist vererbt werden aus, es ziemlich einfach ist , sicher zu stellen , dass es nicht innerhalb der Klasse geändert wird.

Zweitens toString()ergibt sich ein etwas anderes Ergebnis als bei einem _valueNamen value, aber es ist konsistent und TestData(0).toString() == TestData(-1).toString().

schatten
quelle
@ spierce7 Nein, das ist es nicht. _valuewird im Init-Block geändert equalsund hashCode ist nicht defekt.
schatten