Ignora getter per la classe di dati Kotlin


94

Data la seguente classe Kotlin:

data class Test(val value: Int)

Come sovrascriverei il Intgetter in modo che restituisca 0 se il valore è negativo?

Se ciò non è possibile, quali sono alcune tecniche per ottenere un risultato adeguato?


13
Considera la possibilità di modificare la struttura del codice in modo che i valori negativi vengano convertiti in 0 quando viene creata un'istanza della classe e non in un getter. Se sovrascrivi il getter come descritto nella risposta di seguito, tutti gli altri metodi generati come equals (), toString () e l'accesso ai componenti useranno ancora il valore negativo originale, il che probabilmente porterà a un comportamento sorprendente.
yole

Risposte:


138

Dopo aver trascorso quasi un anno intero a scrivere quotidianamente Kotlin, ho scoperto che tentare di sovrascrivere classi di dati come questa è una cattiva pratica. Ci sono 3 approcci validi a questo, e dopo averli presentati, spiegherò perché l'approccio suggerito da altre risposte è sbagliato.

  1. Avere la logica aziendale che crea l' data classalterazione del valore in modo che sia 0 o maggiore prima di chiamare il costruttore con il valore errato. Questo è probabilmente l'approccio migliore per la maggior parte dei casi.

  2. Non utilizzare un file data class. Usa un normale classe chiedi al tuo IDE di generare i metodi equalse hashCodeper te (o no, se non ne hai bisogno). Sì, dovrai rigenerarlo se una qualsiasi delle proprietà viene modificata sull'oggetto, ma ti rimane il controllo totale dell'oggetto.

    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. Crea una proprietà sicura aggiuntiva sull'oggetto che fa ciò che desideri invece di avere un valore privato che viene effettivamente ignorato.

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

Un cattivo approccio che altre risposte suggeriscono:

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

Il problema con questo approccio è che le classi di dati non sono realmente pensate per alterare dati come questo. Sono davvero solo per contenere i dati. Ignorare il getter per una classe di dati come questa significherebbe che Test(0)e Test(-1)non sarebbero l' equalun l'altro e avrebbero hashCodes diversi , ma quando si chiama .value, avrebbero lo stesso risultato. Questo è incoerente e, sebbene possa funzionare per te, altre persone del tuo team che vedono che questa è una classe di dati, potrebbero accidentalmente usarla in modo improprio senza rendersi conto di come l'hai modificata / fatta non funzionare come previsto (cioè questo approccio non lo farebbe t funzionare correttamente in Mapao a Set).


che dire delle classi di dati utilizzate per la serializzazione / deserializzazione, appiattendo una struttura nidificata? Ad esempio, ho appena scritto data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, e lo considero abbastanza buono per il mio caso, tbh. Cosa ne pensi di questo? (c'erano ofc altri campi e quindi credo che non avesse senso per me ricreare quella struttura json nidificata nel mio codice)
Antek

@Antek Dato che non stai alterando i dati, non vedo nulla di sbagliato in questo approccio. Menzionerò anche che il motivo per cui lo stai facendo è perché il modello lato server che ti viene inviato non è conveniente da usare sul client. Per contrastare tali situazioni, il mio team crea un modello lato client in cui traduciamo il modello lato server dopo la deserializzazione. Racchiudiamo tutto questo in un'API lato client. Una volta che inizi a ottenere esempi più complicati di quelli che hai mostrato, questo approccio è molto utile in quanto protegge il client da decisioni / API sbagliate sul modello di server.
spierce7

Non sono d'accordo con quello che affermi di essere "l'approccio migliore". Il problema che vedo è che è molto comune voler impostare un valore in una classe di dati e non cambiarlo mai. Ad esempio, l'analisi di una stringa in un int. Getter / setter personalizzati su una classe di dati non sono solo utili, ma anche necessari; altrimenti, ti rimangono POJO di Java bean che non fanno nulla e il loro comportamento + convalida è contenuto in qualche altra classe.
Abhijit Sarkar

Quello che ho detto è "Questo è probabilmente l'approccio migliore per la maggior parte dei casi". Nella maggior parte dei casi, a meno che non si presentino determinate circostanze, gli sviluppatori dovrebbero avere una netta separazione tra il loro modello e algoritmo / logica di business, dove il modello risultante dal loro algoritmo rappresenta chiaramente i vari stati dei possibili risultati. Kotlin è fantastico per questo, con classi sigillate e classi di dati. Per il tuo esempio parsing a string into an int, stai chiaramente consentendo la logica aziendale dell'analisi e della gestione degli errori di stringhe non numeriche nella tua classe modello ...
spierce7

... La pratica di confondere il confine tra modello e logica aziendale porta sempre a codice meno gestibile, e direi che è un anti-pattern. Probabilmente il 99% delle classi di dati che creo sono setter immutabili / mancanti. Penso che ti piacerebbe davvero dedicare del tempo alla lettura dei vantaggi del tuo team mantenendo immutabili i suoi modelli. Con i modelli immutabili posso garantire che i miei modelli non vengano modificati accidentalmente in qualche altro posto casuale nel codice, il che riduce gli effetti collaterali e, di nuovo, porta a codice gestibile. cioè Kotlin non si è separato Liste MutableListsenza motivo.
spierce7

31

Potresti provare qualcosa di simile:

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 una classe di dati è necessario contrassegnare i parametri del costruttore principale con valo var.

  • Assegno il valore di _valuea valueper utilizzare il nome desiderato per la proprietà.

  • Ho definito una funzione di accesso personalizzata per la proprietà con la logica che hai descritto.


1
Ho ricevuto un errore su IDE, dice "L'inizializzatore non è consentito qui poiché questa proprietà non ha un campo di supporto"
Cheng

6

La risposta dipende dalle funzionalità effettivamente utilizzate che datafornisce. @EPadron ha menzionato un trucco ingegnoso (versione migliorata):

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

Quella volontà funziona come previsto, ei ha un campo, un getter, a destra equals, hashcodee component1. Il problema è quello toStringe copysono strani:

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

Per risolvere il problema con toStringte puoi ridefinirlo a mano. Non conosco alcun modo per correggere la denominazione dei parametri ma non per utilizzarla dataaffatto.


2

So che questa è una vecchia domanda ma sembra che nessuno abbia menzionato la possibilità di rendere il valore privato e scrivere getter personalizzati in questo modo:

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

Questo dovrebbe essere perfettamente valido poiché Kotlin non genererà un getter predefinito per il campo privato.

Ma per il resto sono assolutamente d'accordo con spierce7 sul fatto che le classi di dati servono a contenere i dati e dovresti evitare di codificare la logica "aziendale" lì.


Sono d'accordo con la tua soluzione ma rispetto al codice dovresti chiamarla così val value = test.getValue() e non come gli altri getter val value = test.value
gori

Sì. È corretto. È un po 'diverso se lo chiami da Java perché è sempre .getValue()

1

Ho visto la tua risposta, sono d'accordo sul fatto che le classi di dati siano pensate per contenere solo dati, ma a volte abbiamo bisogno di ricavarne qualcosa.

Ecco cosa sto facendo con la mia classe di dati, ho cambiato alcune proprietà da val a var e le ho sovrascritte nel costruttore.

così:

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

Rendere i campi mutabili solo per poterli modificare durante l'inizializzazione è una cattiva pratica. Sarebbe meglio rendere privato il costruttore e quindi creare una funzione che funge da costruttore (cioè fun Recording(...): Recording { ... }). Inoltre, forse una classe di dati non è ciò che desideri, poiché con classi non di dati puoi separare le tue proprietà dai parametri del costruttore. È meglio essere espliciti con le tue intenzioni di mutabilità nella definizione della tua classe. Se anche questi campi sono modificabili comunque, allora una classe di dati va bene, ma quasi tutte le mie classi di dati sono immutabili.
spierce7

@ spierce7 è davvero così brutto meritare un voto negativo? Ad ogni modo, questa soluzione mi sta bene, non richiede così tanto codice e mantiene intatti l'hash e gli uguali.
Simou

0

Questo sembra essere uno (tra gli altri) fastidiosi inconvenienti di Kotlin.

Sembra che l'unica soluzione ragionevole, che mantiene completamente la compatibilità con le versioni precedenti della classe, sia quella di convertirla in una classe normale (non una classe "dati"), e implementare a mano (con l'aiuto dell'IDE) i metodi: hashCode ( ), equals (), toString (), copy () e 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)
    }

}

Non sono sicuro che lo definirei un inconveniente. È semplicemente una limitazione della funzionalità della classe di dati, che non è una funzionalità offerta da Java.
spierce7

0

Ho trovato il seguente approccio migliore per ottenere ciò di cui hai bisogno senza interruzioni equalse 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())

Però,

Innanzitutto, nota che lo _valueè var, no val, ma d'altra parte, poiché è privato e le classi di dati non possono essere ereditate, è abbastanza facile assicurarsi che non venga modificato all'interno della classe.

In secondo luogo, toString()produce un risultato leggermente diverso da quello che avrebbe se _valuefosse stato nominato value, ma è coerente e TestData(0).toString() == TestData(-1).toString().


@ spierce7 No, non lo è. _valueviene modificato nel blocco di inizializzazione equalse hashCode non sono interrotti.
schatten
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.