Considere cambiar la estructura de su código para que los valores negativos se conviertan a 0 cuando se crea una instancia de la clase, y no en un captador. Si anula el getter como se describe en la respuesta a continuación, todos los demás métodos generados como equals (), toString () y el acceso al componente seguirán usando el valor negativo original, lo que probablemente dará lugar a un comportamiento sorprendente.
yole
Respuestas:
148
Después de pasar casi un año completo escribiendo Kotlin a diario, descubrí que intentar anular clases de datos como esta es una mala práctica. Hay 3 enfoques válidos para esto, y después de presentarlos, explicaré por qué el enfoque que han sugerido otras respuestas es malo.
Haga que la lógica de su negocio que crea la data classmodificación del valor sea 0 o mayor antes de llamar al constructor con el valor incorrecto. Este es probablemente el mejor enfoque para la mayoría de los casos.
No use un data class. Use un método regular classy haga que su IDE genere los métodos equalsy hashCodepor usted (o no lo haga, si no los necesita). Sí, tendrá que volver a generarlo si se cambia alguna de las propiedades en el objeto, pero se queda con el control total del objeto.
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()
}
}
Cree una propiedad segura adicional en el objeto que haga lo que desee en lugar de tener un valor privado que se anule efectivamente.
dataclassTest(val value: Int) {
val safeValue: Intget() = if (value < 0) 0else value
}
Un mal enfoque que sugieren otras respuestas:
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
El problema con este enfoque es que las clases de datos no están diseñadas para alterar datos como este. Realmente son solo para almacenar datos. Anular el captador para una clase de datos como esta significaría que Test(0)y Test(-1)no lo harían equalentre sí y tendrían diferentes hashCodes, pero cuando llamas .value, tendrían el mismo resultado. Esto es inconsistente y, si bien puede funcionar para usted, otras personas de su equipo que ven que esta es una clase de datos, pueden hacer un mal uso accidental sin darse cuenta de cómo lo ha alterado / hecho que no funcione como se esperaba (es decir, este enfoque no funcionaría) t funciona correctamente en a Mapo a Set).
¿Qué pasa con las clases de datos utilizadas para la serialización / deserialización, aplanar una estructura anidada? Por ejemplo, acabo de escribir data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, y lo considero bastante bueno para mi caso, tbh. ¿Qué piensas sobre esto? (había ofc otros campos y, por lo tanto, creo que no tenía sentido para mí recrear esa estructura json anidada en mi código)
Antek
@Antek Dado que no está alterando los datos, no veo nada malo en este enfoque. También mencionaré que la razón por la que está haciendo esto es porque el modelo del lado del servidor que le están enviando no es conveniente para usar en el cliente. Para contrarrestar tales situaciones, mi equipo crea un modelo del lado del cliente al que traducimos el modelo del lado del servidor después de la deserialización. Resolvemos todo esto en una API del lado del cliente. Una vez que comience a obtener ejemplos que son más complicados de lo que ha mostrado, este enfoque es muy útil ya que protege al cliente de malas decisiones / apis del modelo de servidor.
spierce7
No estoy de acuerdo con lo que afirma ser el "mejor enfoque". El problema que veo es que es muy común querer establecer un valor en una clase de datos y nunca cambiarlo. Por ejemplo, al analizar una cadena en un int. Los getters / setters personalizados en una clase de datos no solo son útiles, sino también necesarios; de lo contrario, se quedan con los POJO de Java bean que no hacen nada y su comportamiento + validación está contenido en alguna otra clase.
Abhijit Sarkar
Lo que dije es "Este es probablemente el mejor enfoque para la mayoría de los casos". En la mayoría de los casos, a menos que surjan ciertas circunstancias, los desarrolladores deben tener una separación clara entre su modelo y el algoritmo / lógica comercial, donde el modelo resultante de su algoritmo representa claramente los diversos estados de los posibles resultados. Kotlin es fantástico para esto, con clases selladas y clases de datos. Para su ejemplo de parsing a string into an int, claramente está permitiendo la lógica empresarial del análisis y el manejo de errores de cadenas no numéricas en su clase de modelo ...
spierce7
... La práctica de enturbiar la línea entre el modelo y la lógica empresarial siempre conduce a un código menos mantenible, y yo diría que es un anti-patrón. Probablemente el 99% de las clases de datos que creo son setters inmutables / carentes. Creo que realmente disfrutaría de tomarse un tiempo para leer sobre los beneficios de que su equipo mantenga sus modelos inmutables. Con modelos inmutables, puedo garantizar que mis modelos no se modifiquen accidentalmente en algún otro lugar aleatorio del código, lo que reduce los efectos secundarios y, nuevamente, conduce a un código mantenible. es decir, Kotlin no se separó Listy MutableListsin motivo.
spierce7
31
Podrías probar algo como esto:
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
En una clase de datos, debe marcar los parámetros del constructor principal con valo var.
Estoy asignando el valor de _valuea valuepara usar el nombre deseado para la propiedad.
Definí un acceso personalizado para la propiedad con la lógica que describiste.
Recibí un error en IDE, dice "El inicializador no está permitido aquí porque esta propiedad no tiene un campo de respaldo"
Cheng
6
La respuesta depende de las capacidades que realmente utilice data. @EPadron mencionó un truco ingenioso (versión mejorada):
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
Esa voluntad funciona como se espera, la IE tiene un campo, un captador, a la derecha equals, hashcodey component1. El problema es ese toStringy copyson raros:
Para solucionar el problema toString, puede redefinirlo con las manos. No conozco ninguna forma de arreglar el nombre de los parámetros, pero no usarlos dataen absoluto.
Sé que esta es una pregunta antigua, pero parece que nadie mencionó la posibilidad de hacer que el valor sea privado y escribir un getter personalizado de esta manera:
dataclassTest(privateval value: Int) {
fungetValue(): Int = if (value < 0) 0else value
}
Esto debería ser perfectamente válido ya que Kotlin no generará un getter predeterminado para el campo privado.
Pero por lo demás, definitivamente estoy de acuerdo con spierce7 en que las clases de datos son para almacenar datos y usted debe evitar codificar la lógica "comercial" allí.
Estoy de acuerdo con su solución, pero en el código tendrías que llamarlo así val value = test.getValue() y no como otros captadores val value = test.value
gori
Si. Eso es correcto. Es un poco diferente si lo llama desde Java, ya que siempre está.getValue()
bio007
1
He visto su respuesta, estoy de acuerdo en que las clases de datos están destinadas solo a almacenar datos, pero a veces necesitamos hacer algo con ellas.
Esto es lo que estoy haciendo con mi clase de datos, cambié algunas propiedades de val a var y las superpuse en el constructor.
al igual que:
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)
}
}
Hacer que los campos sean mutables solo para poder modificarlos durante la inicialización es una mala práctica. Sería mejor hacer que el constructor sea privado y luego crear una función que actúe como un constructor (es decir fun Recording(...): Recording { ... }). Además, tal vez una clase de datos no sea lo que desea, ya que con las clases que no son de datos puede separar sus propiedades de los parámetros de su constructor. Es mejor ser explícito con sus intenciones de mutabilidad en la definición de su clase. Si esos campos también son mutables de todos modos, entonces una clase de datos está bien, pero casi todas mis clases de datos son inmutables.
spierce7
@ spierce7 ¿es realmente tan malo merecer un voto en contra? De todos modos, esta solución me queda bien, no requiere tanta codificación y mantiene intactos el hash y los iguales.
Simou
0
Este parece ser uno (entre otros) inconvenientes molestos de Kotlin.
Parece que la única solución razonable, que mantiene completamente la compatibilidad con versiones anteriores de la clase, es convertirla en una clase regular (no una clase de "datos") e implementar manualmente (con la ayuda del IDE) los métodos: hashCode ( ), equals (), toString (), copy () y 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)
}
}
No estoy seguro de que llamaría a esto un inconveniente. Es simplemente una limitación de la característica de clase de datos, que no es una característica que ofrece Java.
spierce7
0
Encontré que lo siguiente es el mejor enfoque para lograr lo que necesita sin romperse equalsy hashCode:
En primer lugar, cabe destacar que _valuees var, no val, pero por otro lado, ya que es privado y clases de datos no puede ser heredada de, que es bastante fácil para asegurarse de que no se modifica dentro de la clase.
En segundo lugar, toString()produce un resultado ligeramente diferente al que obtendría si tuviera _valueun nombre value, pero es coherente y TestData(0).toString() == TestData(-1).toString().
Respuestas:
Después de pasar casi un año completo escribiendo Kotlin a diario, descubrí que intentar anular clases de datos como esta es una mala práctica. Hay 3 enfoques válidos para esto, y después de presentarlos, explicaré por qué el enfoque que han sugerido otras respuestas es malo.
Haga que la lógica de su negocio que crea la
data class
modificación del valor sea 0 o mayor antes de llamar al constructor con el valor incorrecto. Este es probablemente el mejor enfoque para la mayoría de los casos.No use un
data class
. Use un método regularclass
y haga que su IDE genere los métodosequals
yhashCode
por usted (o no lo haga, si no los necesita). Sí, tendrá que volver a generarlo si se cambia alguna de las propiedades en el objeto, pero se queda con el control total del objeto.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() } }
Cree una propiedad segura adicional en el objeto que haga lo que desee en lugar de tener un valor privado que se anule efectivamente.
data class Test(val value: Int) { val safeValue: Int get() = if (value < 0) 0 else value }
Un mal enfoque que sugieren otras respuestas:
data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
El problema con este enfoque es que las clases de datos no están diseñadas para alterar datos como este. Realmente son solo para almacenar datos. Anular el captador para una clase de datos como esta significaría que
Test(0)
yTest(-1)
no lo haríanequal
entre sí y tendrían diferenteshashCode
s, pero cuando llamas.value
, tendrían el mismo resultado. Esto es inconsistente y, si bien puede funcionar para usted, otras personas de su equipo que ven que esta es una clase de datos, pueden hacer un mal uso accidental sin darse cuenta de cómo lo ha alterado / hecho que no funcione como se esperaba (es decir, este enfoque no funcionaría) t funciona correctamente en aMap
o aSet
).fuente
data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }
, y lo considero bastante bueno para mi caso, tbh. ¿Qué piensas sobre esto? (había ofc otros campos y, por lo tanto, creo que no tenía sentido para mí recrear esa estructura json anidada en mi código)parsing a string into an int
, claramente está permitiendo la lógica empresarial del análisis y el manejo de errores de cadenas no numéricas en su clase de modelo ...List
yMutableList
sin motivo.Podrías probar algo como esto:
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
En una clase de datos, debe marcar los parámetros del constructor principal con
val
ovar
.Estoy asignando el valor de
_value
avalue
para usar el nombre deseado para la propiedad.Definí un acceso personalizado para la propiedad con la lógica que describiste.
fuente
La respuesta depende de las capacidades que realmente utilice
data
. @EPadron mencionó un truco ingenioso (versión mejorada):data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
Esa voluntad funciona como se espera, la IE tiene un campo, un captador, a la derecha
equals
,hashcode
ycomponent1
. El problema es esetoString
ycopy
son raros:println(Test(1)) // prints: Test(_value=1) Test(1).copy(_value = 5) // <- weird naming
Para solucionar el problema
toString
, puede redefinirlo con las manos. No conozco ninguna forma de arreglar el nombre de los parámetros, pero no usarlosdata
en absoluto.fuente
Sé que esta es una pregunta antigua, pero parece que nadie mencionó la posibilidad de hacer que el valor sea privado y escribir un getter personalizado de esta manera:
data class Test(private val value: Int) { fun getValue(): Int = if (value < 0) 0 else value }
Esto debería ser perfectamente válido ya que Kotlin no generará un getter predeterminado para el campo privado.
Pero por lo demás, definitivamente estoy de acuerdo con spierce7 en que las clases de datos son para almacenar datos y usted debe evitar codificar la lógica "comercial" allí.
fuente
val value = test.getValue()
y no como otros captadoresval value = test.value
.getValue()
He visto su respuesta, estoy de acuerdo en que las clases de datos están destinadas solo a almacenar datos, pero a veces necesitamos hacer algo con ellas.
Esto es lo que estoy haciendo con mi clase de datos, cambié algunas propiedades de val a var y las superpuse en el constructor.
al igual que:
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) } }
fuente
fun Recording(...): Recording { ... }
). Además, tal vez una clase de datos no sea lo que desea, ya que con las clases que no son de datos puede separar sus propiedades de los parámetros de su constructor. Es mejor ser explícito con sus intenciones de mutabilidad en la definición de su clase. Si esos campos también son mutables de todos modos, entonces una clase de datos está bien, pero casi todas mis clases de datos son inmutables.Este parece ser uno (entre otros) inconvenientes molestos de Kotlin.
Parece que la única solución razonable, que mantiene completamente la compatibilidad con versiones anteriores de la clase, es convertirla en una clase regular (no una clase de "datos") e implementar manualmente (con la ayuda del IDE) los métodos: hashCode ( ), equals (), toString (), copy () y 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) } }
fuente
Encontré que lo siguiente es el mejor enfoque para lograr lo que necesita sin romperse
equals
yhashCode
: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())
Sin embargo,
En primer lugar, cabe destacar que
_value
esvar
, noval
, pero por otro lado, ya que es privado y clases de datos no puede ser heredada de, que es bastante fácil para asegurarse de que no se modifica dentro de la clase.En segundo lugar,
toString()
produce un resultado ligeramente diferente al que obtendría si tuviera_value
un nombrevalue
, pero es coherente yTestData(0).toString() == TestData(-1).toString()
.fuente
_value
se está modificando en el bloque initequals
yhashCode
no están rotos.