Compte tenu de la classe Kotlin suivante:
data class Test(val value: Int)
Comment remplacer le Int
getter pour qu'il renvoie 0 si la valeur est négative?
Si ce n'est pas possible, quelles sont les techniques pour obtenir un résultat approprié?
Compte tenu de la classe Kotlin suivante:
data class Test(val value: Int)
Comment remplacer le Int
getter pour qu'il renvoie 0 si la valeur est négative?
Si ce n'est pas possible, quelles sont les techniques pour obtenir un résultat approprié?
Réponses:
Après avoir passé presque un an à écrire quotidiennement Kotlin, j'ai trouvé que tenter de remplacer des classes de données comme celle-ci est une mauvaise pratique. Il y a 3 approches valables à cela, et après les avoir présentées, je vais vous expliquer pourquoi l'approche suggérée par d'autres réponses est mauvaise.
Demandez à votre logique métier qui crée la data class
valeur de modifier la valeur 0 ou supérieure avant d'appeler le constructeur avec la valeur incorrecte. C'est probablement la meilleure approche dans la plupart des cas.
N'utilisez pas de fichier data class
. Utilisez un standard class
et demandez à votre IDE de générer les méthodes equals
et hashCode
pour vous (ou non, si vous n'en avez pas besoin). Oui, vous devrez le régénérer si l'une des propriétés est modifiée sur l'objet, mais vous vous retrouvez avec le contrôle total de l'objet.
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()
}
}
Créez une propriété sûre supplémentaire sur l'objet qui fait ce que vous voulez au lieu d'avoir une valeur privée qui est effectivement remplacée.
data class Test(val value: Int) {
val safeValue: Int
get() = if (value < 0) 0 else value
}
Une mauvaise approche que suggèrent d'autres réponses:
data class Test(private val _value: Int) {
val value: Int
get() = if (_value < 0) 0 else _value
}
Le problème avec cette approche est que les classes de données ne sont pas vraiment destinées à modifier des données comme celle-ci. Ils sont vraiment juste pour contenir des données. Remplacer le getter pour une classe de données comme celle-ci signifierait cela Test(0)
et Test(-1)
ne le ferait pas l' equal
un l'autre et aurait des hashCode
s différents , mais lorsque vous appelez .value
, ils auraient le même résultat. Ceci est incohérent, et bien que cela puisse fonctionner pour vous, d'autres personnes de votre équipe qui voient qu'il s'agit d'une classe de données peuvent accidentellement en abuser sans se rendre compte de la façon dont vous l'avez modifiée / empêchée de fonctionner comme prévu (c'est-à-dire que cette approche ne fonctionnerait pas. t fonctionnent correctement en a Map
ou a Set
).
data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }
, et je considère que c'est assez bon pour mon cas, tbh. Que pensez-vous de ceci? (il y avait souvent d'autres champs et par conséquent, je pense que cela n'avait aucun sens pour moi de recréer cette structure json imbriquée dans mon code)
parsing a string into an int
, vous autorisez clairement la logique métier d'analyse et de gestion des erreurs des chaînes non numériques dans votre classe de modèle ...
List
et MutableList
sans raison.
Vous pouvez essayer quelque chose comme ceci:
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
Dans une classe de données, vous devez marquer les paramètres du constructeur principal avec val
ou var
.
J'attribue la valeur de _value
à value
afin d'utiliser le nom souhaité pour la propriété.
J'ai défini un accesseur personnalisé pour la propriété avec la logique que vous avez décrite.
La réponse dépend des fonctionnalités que vous utilisez réellement data
. @EPadron a mentionné une astuce astucieuse (version améliorée):
data class Test(private val _value: Int) {
val value: Int
get() = if (_value < 0) 0 else _value
}
Cette volonté fonctionne comme prévu, ei il a un champ, un getter, à droite equals
, hashcode
et component1
. Le hic, c'est ça toString
et copy
c'est bizarre:
println(Test(1)) // prints: Test(_value=1)
Test(1).copy(_value = 5) // <- weird naming
Pour résoudre le problème, toString
vous pouvez le redéfinir à la main. Je ne connais aucun moyen de corriger la dénomination des paramètres mais de ne pas l'utiliser data
du tout.
Je sais que c'est une vieille question mais il semble que personne n'ait mentionné la possibilité de rendre la valeur privée et d'écrire un getter personnalisé comme celui-ci:
data class Test(private val value: Int) {
fun getValue(): Int = if (value < 0) 0 else value
}
Cela devrait être parfaitement valide car Kotlin ne générera pas de getter par défaut pour le champ privé.
Mais sinon, je suis tout à fait d'accord avec spierce7 pour dire que les classes de données sont destinées à contenir des données et que vous devriez éviter de coder en dur la logique «métier» là-bas.
val value = test.getValue()
et pas comme les autres getters val value = test.value
.getValue()
J'ai vu votre réponse, je suis d'accord que les classes de données sont destinées à contenir des données uniquement, mais parfois nous devons en faire quelque chose.
Voici ce que je fais avec ma classe de données, j'ai changé certaines propriétés de val en var, et les ai overid dans le constructeur.
ainsi:
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)
}
}
fun Recording(...): Recording { ... }
). Une classe de données n'est peut-être pas non plus ce que vous voulez, car avec des classes sans données, vous pouvez séparer vos propriétés de vos paramètres de constructeur. Il vaut mieux être explicite avec vos intentions de mutabilité dans votre définition de classe. Si ces champs sont également modifiables de toute façon, alors une classe de données convient, mais presque toutes mes classes de données sont immuables.
Cela semble être l'un (parmi d'autres) inconvénients gênants de Kotlin.
Il semble que la seule solution raisonnable, qui garde complètement la rétrocompatibilité de la classe, est de la convertir en une classe régulière (pas une classe "data"), et d'implémenter à la main (à l'aide de l'EDI) les méthodes: hashCode ( ), equals (), toString (), copy () et 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)
}
}
J'ai trouvé que ce qui suit était la meilleure approche pour réaliser ce dont vous avez besoin sans vous casser equals
et 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())
cependant,
Tout d'abord, notez que ce _value
n'est var
pas le cas val
, mais d'un autre côté, puisque c'est privé et que les classes de données ne peuvent pas être héritées, il est assez facile de s'assurer qu'il n'est pas modifié dans la classe.
Deuxièmement, toString()
produit un résultat légèrement différent de ce qu'il serait si _value
était nommé value
, mais il est cohérent et TestData(0).toString() == TestData(-1).toString()
.
_value
est en cours de modification dans le bloc d'initialisation et equals
et hashCode
ne sont pas interrompus.