Anular el captador para la clase de datos de Kotlin

99

Dada la siguiente clase de Kotlin:

data class Test(val value: Int)

¿Cómo anularía el Intcaptador para que devuelva 0 si el valor es negativo?

Si esto no es posible, ¿cuáles son algunas técnicas para lograr un resultado adecuado?

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

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

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

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

spierce7
fuente
¿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:

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

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

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, hashcodey component1. El problema es ese toStringy copyson 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 usarlos dataen absoluto.

voddan
fuente
2

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

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

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
fuente
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 ()

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
fuente
1
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:

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

charlar
fuente
@ spierce7 No, no lo es. _valuese está modificando en el bloque init equalsy hashCode no están rotos.
schatten
¿pueden ayudarme con esto? stackoverflow.com/questions/64015108/…
WISHY