Considere alterar a estrutura do seu código para que os valores negativos sejam convertidos em 0 quando a classe for instanciada, e não em um getter. Se você substituir o getter conforme descrito na resposta abaixo, todos os outros métodos gerados, como equals (), toString () e acesso ao componente ainda usarão o valor negativo original, o que provavelmente levará a um comportamento surpreendente.
yole
Respostas:
138
Depois de passar quase um ano inteiro escrevendo Kotlin diariamente, descobri que tentar substituir as classes de dados dessa maneira é uma prática ruim. Existem 3 abordagens válidas para isso e, depois de apresentá-las, explicarei por que a abordagem que outras respostas sugeriram é ruim.
Tenha sua lógica de negócios que cria o data classalter o valor para ser 0 ou maior antes de chamar o construtor com o valor incorreto. Esta é provavelmente a melhor abordagem para a maioria dos casos.
Não use um data class. Use um regular classe faça com que seu IDE gere os métodos equalse hashCodepara você (ou não, se não precisar deles). Sim, você terá que gerá-lo novamente se alguma das propriedades for alterada no objeto, mas você terá controle total do objeto.
classTest(value:Int){val value:Int= value
get()=if(field <0)0else field
overridefun equals(other:Any?):Boolean{if(this=== other)returntrueif(other !isTest)returnfalsereturntrue}overridefun hashCode():Int{return javaClass.hashCode()}}
Crie uma propriedade de segurança adicional no objeto que faz o que você deseja, em vez de ter um valor privado que é efetivamente substituído.
data classTest(val value:Int){val safeValue:Intget()=if(value <0)0else value
}
Uma abordagem ruim que outras respostas estão sugerindo:
data classTest(privateval_value:Int){val value:Intget()=if(_value <0)0else_value
}
O problema com essa abordagem é que as classes de dados não são realmente destinadas a alterar dados como este. Eles servem apenas para armazenar dados. Substituir o getter para uma classe de dados como essa significaria isso Test(0)e Test(-1)não equalum ao outro e teria hashCodes diferentes , mas quando você chamasse .value, eles teriam o mesmo resultado. Isso é inconsistente e, embora possa funcionar para você, outras pessoas em sua equipe que veem que se trata de uma classe de dados podem acidentalmente usá-la indevidamente, sem perceber como você a alterou / fez com que não funcionasse como esperado (ou seja, essa abordagem não funcionaria t funcionar corretamente em a Mapou a Set).
e as classes de dados usadas para serialização / desserialização, achatando uma estrutura aninhada? Por exemplo, acabei de escrever data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, e considero muito bom para o meu caso, tbh. O que você pensa sobre isso? (havia outros campos ofc e, portanto, acredito que não fazia sentido para mim recriar essa estrutura json aninhada em meu código)
Antek
@Antek Visto que você não está alterando os dados, não vejo nada de errado com essa abordagem. Mencionarei também que você está fazendo isso porque o modelo do lado do servidor que você está recebendo não é conveniente para usar no cliente. Para combater essas situações, minha equipe cria um modelo do lado do cliente para o qual traduzimos o modelo do lado do servidor após a desserialização. Envolvemos tudo isso em uma API do lado do cliente. Depois de começar a obter exemplos mais complicados do que os que você mostrou, essa abordagem é muito útil, pois protege o cliente de decisões / apis de modelos de servidor incorretos.
spierce7
Não concordo com o que você afirma ser a "melhor abordagem". O problema que vejo é que é muito comum querer definir um valor em uma classe de dados e nunca alterá-lo. Por exemplo, analisar uma string em um int. Os getters / setters personalizados em uma classe de dados não são apenas úteis, mas também necessários; caso contrário, você fica com POJOs de bean Java que não fazem nada e seu comportamento + validação está contido em alguma outra classe.
Abhijit Sarkar
O que eu disse é "Esta é provavelmente a melhor abordagem para a maioria dos casos". Na maioria dos casos, a menos que surjam certas circunstâncias, os desenvolvedores devem ter uma separação clara entre seu modelo e algoritmo / lógica de negócios, onde o modelo resultante de seu algoritmo representa claramente os vários estados dos resultados possíveis. Kotlin é fantástico para isso, com classes seladas e classes de dados. Para o seu exemplo de parsing a string into an int, você está permitindo claramente a lógica de negócios de análise e tratamento de erros não numéricos em sua classe de modelo ...
spierce7
... A prática de confundir a linha entre o modelo e a lógica de negócios sempre leva a um código menos sustentável, e eu diria que é um antipadrão. Provavelmente 99% das classes de dados que crio são configuradores imutáveis / ausentes. Acho que você realmente gostaria de ler sobre os benefícios de sua equipe manter seus modelos imutáveis. Com modelos imutáveis, posso garantir que meus modelos não sejam modificados acidentalmente em algum outro lugar aleatório no código, o que reduz os efeitos colaterais e, novamente, leva a um código sustentável. ou seja, Kotlin não se separou Liste MutableListsem motivo.
spierce7
31
Você pode tentar algo assim:
data classTest(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
Em uma classe de dados, você deve marcar os parâmetros do construtor primário com valou var.
Estou atribuindo o valor de _valuea valuepara usar o nome desejado para a propriedade.
Eu defini um acessador personalizado para a propriedade com a lógica que você descreveu.
Para corrigir o problema, toStringvocê pode redefini-lo manualmente. Não conheço nenhuma maneira de corrigir a nomenclatura do parâmetro, mas não de usá data-la.
Eu sei que esta é uma questão antiga, mas parece que ninguém mencionou a possibilidade de tornar o valor privado e escrever um getter personalizado como este:
data classTest(privateval value:Int){fun getValue():Int=if(value <0)0else value
}
Isso deve ser perfeitamente válido, pois o Kotlin não gerará um getter padrão para o campo privado.
Mas, caso contrário, eu definitivamente concordo com spierce7 que as classes de dados são para armazenar dados e você deve evitar codificar a lógica de "negócios" aí.
Eu concordo com sua solução, mas que no código você teria que chamá-lo assim val value = test.getValue() e não como outros getters val value = test.value
gori
Sim. Está correto. É um pouco diferente se você chamá-lo de Java, pois está sempre.getValue()
bio007
1
Eu vi sua resposta, concordo que as classes de dados são destinadas a conter apenas dados, mas às vezes precisamos fazer algumas coisas com elas.
Aqui está o que estou fazendo com minha classe de dados, alterei algumas propriedades de val para var e as substituí no construtor.
Tornar os campos mutáveis apenas para que você possa modificá-los durante a inicialização é uma prática ruim. Seria melhor tornar o construtor privado e, em seguida, criar uma função que atue como um construtor (ou seja fun Recording(...): Recording { ... }). Além disso, talvez uma classe de dados não seja o que você deseja, já que com classes que não são de dados, você pode separar suas propriedades dos parâmetros do seu construtor. É melhor ser explícito com suas intenções de mutabilidade em sua definição de classe. Se esses campos também forem mutáveis de qualquer maneira, uma classe de dados está bem, mas quase todas as minhas classes de dados são imutáveis.
spierce7
@ spierce7 é realmente tão ruim merecer um voto negativo? De qualquer forma, essa solução me serve bem, não requer tanta codificação e mantém o hash e igual intactos.
Simou
0
Esta parece ser uma (entre outras) desvantagens irritantes do Kotlin.
Parece que a única solução razoável, que mantém completamente a compatibilidade com versões anteriores da classe, é convertê-la em uma classe regular (não uma classe de "dados") e implementar manualmente (com a ajuda do IDE) os métodos: hashCode ( ), equals (), toString (), copy () e componentN ()
classData3(i:Int){var i:Int= i
overridefun equals(other:Any?):Boolean{if(this=== other)returntrueif(other?.javaClass != javaClass)returnfalse
other asData3if(i != other.i)returnfalsereturntrue}overridefun hashCode():Int{return i
}overridefun toString():String{return"Data3(i=$i)"}fun component1():Int= i
fun copy(i:Int=this.i):Data3{returnData3(i)}}
Não tenho certeza se chamaria isso de desvantagem. É apenas uma limitação do recurso de classe de dados, que não é um recurso que o Java oferece.
spierce7
0
Achei o seguinte a melhor abordagem para conseguir o que você precisa sem quebrar equalse hashCode:
data classTestData(privatevar_value:Int){init{_value =if(_value <0)0else_value
}val value:Intget()=_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())
Contudo,
Primeiro, observe que _valueé var, nãoval , mas, por outro lado, uma vez que é privado e classes de dados não pode ser herdada de, é bastante fácil de se certificar de que não é modificada dentro da classe.
Em segundo lugar, toString()produz um resultado ligeiramente diferente do que produziria se _valuefosse nomeado value, mas é consistente e TestData(0).toString() == TestData(-1).toString().
Respostas:
Depois de passar quase um ano inteiro escrevendo Kotlin diariamente, descobri que tentar substituir as classes de dados dessa maneira é uma prática ruim. Existem 3 abordagens válidas para isso e, depois de apresentá-las, explicarei por que a abordagem que outras respostas sugeriram é ruim.
Tenha sua lógica de negócios que cria o
data class
alter o valor para ser 0 ou maior antes de chamar o construtor com o valor incorreto. Esta é provavelmente a melhor abordagem para a maioria dos casos.Não use um
data class
. Use um regularclass
e faça com que seu IDE gere os métodosequals
ehashCode
para você (ou não, se não precisar deles). Sim, você terá que gerá-lo novamente se alguma das propriedades for alterada no objeto, mas você terá controle total do objeto.Crie uma propriedade de segurança adicional no objeto que faz o que você deseja, em vez de ter um valor privado que é efetivamente substituído.
Uma abordagem ruim que outras respostas estão sugerindo:
O problema com essa abordagem é que as classes de dados não são realmente destinadas a alterar dados como este. Eles servem apenas para armazenar dados. Substituir o getter para uma classe de dados como essa significaria isso
Test(0)
eTest(-1)
nãoequal
um ao outro e teriahashCode
s diferentes , mas quando você chamasse.value
, eles teriam o mesmo resultado. Isso é inconsistente e, embora possa funcionar para você, outras pessoas em sua equipe que veem que se trata de uma classe de dados podem acidentalmente usá-la indevidamente, sem perceber como você a alterou / fez com que não funcionasse como esperado (ou seja, essa abordagem não funcionaria t funcionar corretamente em aMap
ou aSet
).fonte
data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }
, e considero muito bom para o meu caso, tbh. O que você pensa sobre isso? (havia outros campos ofc e, portanto, acredito que não fazia sentido para mim recriar essa estrutura json aninhada em meu código)parsing a string into an int
, você está permitindo claramente a lógica de negócios de análise e tratamento de erros não numéricos em sua classe de modelo ...List
eMutableList
sem motivo.Você pode tentar algo assim:
Em uma classe de dados, você deve marcar os parâmetros do construtor primário com
val
ouvar
.Estou atribuindo o valor de
_value
avalue
para usar o nome desejado para a propriedade.Eu defini um acessador personalizado para a propriedade com a lógica que você descreveu.
fonte
A resposta depende de quais recursos você realmente usa que
data
fornece. @EPadron mencionou um truque bacana (versão aprimorada):Isso funcionará conforme o esperado, ei tem um campo, um getter, certo
equals
,hashcode
ecomponent1
. O problema é essetoString
ecopy
são estranhos:Para corrigir o problema,
toString
você pode redefini-lo manualmente. Não conheço nenhuma maneira de corrigir a nomenclatura do parâmetro, mas não de usádata
-la.fonte
Eu sei que esta é uma questão antiga, mas parece que ninguém mencionou a possibilidade de tornar o valor privado e escrever um getter personalizado como este:
Isso deve ser perfeitamente válido, pois o Kotlin não gerará um getter padrão para o campo privado.
Mas, caso contrário, eu definitivamente concordo com spierce7 que as classes de dados são para armazenar dados e você deve evitar codificar a lógica de "negócios" aí.
fonte
val value = test.getValue()
e não como outros gettersval value = test.value
.getValue()
Eu vi sua resposta, concordo que as classes de dados são destinadas a conter apenas dados, mas às vezes precisamos fazer algumas coisas com elas.
Aqui está o que estou fazendo com minha classe de dados, alterei algumas propriedades de val para var e as substituí no construtor.
igual a:
fonte
fun Recording(...): Recording { ... }
). Além disso, talvez uma classe de dados não seja o que você deseja, já que com classes que não são de dados, você pode separar suas propriedades dos parâmetros do seu construtor. É melhor ser explícito com suas intenções de mutabilidade em sua definição de classe. Se esses campos também forem mutáveis de qualquer maneira, uma classe de dados está bem, mas quase todas as minhas classes de dados são imutáveis.Esta parece ser uma (entre outras) desvantagens irritantes do Kotlin.
Parece que a única solução razoável, que mantém completamente a compatibilidade com versões anteriores da classe, é convertê-la em uma classe regular (não uma classe de "dados") e implementar manualmente (com a ajuda do IDE) os métodos: hashCode ( ), equals (), toString (), copy () e componentN ()
fonte
Achei o seguinte a melhor abordagem para conseguir o que você precisa sem quebrar
equals
ehashCode
:Contudo,
Primeiro, observe que
_value
évar
, nãoval
, mas, por outro lado, uma vez que é privado e classes de dados não pode ser herdada de, é bastante fácil de se certificar de que não é modificada dentro da classe.Em segundo lugar,
toString()
produz um resultado ligeiramente diferente do que produziria se_value
fosse nomeadovalue
, mas é consistente eTestData(0).toString() == TestData(-1).toString()
.fonte
_value
está sendo modificado no bloco de inicializaçãoequals
ehashCode
não está quebrado.