Пожалуйста, подумайте об изменении структуры вашего кода, чтобы отрицательные значения преобразовывались в 0 при создании экземпляра класса, а не в геттере. Если вы переопределите получатель, как описано в ответе ниже, все другие сгенерированные методы, такие как equals (), toString () и доступ к компонентам, по-прежнему будут использовать исходное отрицательное значение, что, вероятно, приведет к неожиданному поведению.
yole
Ответы:
148
Потратив почти год на написание Kotlin ежедневно, я обнаружил, что попытки переопределить классы данных, подобные этому, - плохая практика. Есть 3 действительных подхода к этому, и после того, как я их представлю, я объясню, почему подход, предложенный другими ответами, плох.
Имейте свою бизнес-логику, которая создает data classизменение значения на 0 или больше, прежде чем вызывать конструктор с неверным значением. Вероятно, это лучший подход для большинства случаев.
Не используйте data class. Используйте обычный classи ваш IDE генерировать equalsи hashCodeметоды для вас (или нет, если они не нужны). Да, вам придется повторно сгенерировать его, если какое-либо из свойств объекта изменится, но вы остаетесь с полным контролем над объектом.
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()
}
}
Создайте дополнительное безопасное свойство объекта, которое делает то, что вы хотите, вместо того, чтобы иметь частное значение, которое эффективно переопределяет.
dataclassTest(val value: Int) {
val safeValue: Intget() = if (value < 0) 0else value
}
Плохой подход, который предлагают другие ответы:
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
Проблема с этим подходом заключается в том, что классы данных на самом деле не предназначены для такого изменения данных. Они действительно предназначены только для хранения данных. Переопределение получателя для такого класса данных означало бы это, Test(0)а Test(-1)не equalдруг друга, и у них были бы разные hashCodes, но когда вы вызываете .value, они будут иметь тот же результат. Это непоследовательно, и хотя это может сработать для вас, другие люди в вашей команде, которые видят, что это класс данных, могут случайно злоупотребить им, не понимая, как вы его изменили / заставили его работать не так, как ожидалось (т. Е. Этот подход не t работать правильно в а Mapили а Set).
как насчет классов данных, используемых для сериализации / десериализации, выравнивания вложенной структуры? Например, я только что написал data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, и считаю, что это неплохо для моего случая, tbh. Что Вы думаете об этом? (были и другие поля, и поэтому я считаю, что для меня не имело смысла воссоздавать эту вложенную структуру json в моем коде)
Antek
@Antek Учитывая, что вы не изменяете данные, я не вижу ничего плохого в этом подходе. Я также упомяну, что причина, по которой вы это делаете, заключается в том, что модель на стороне сервера, которую вы отправляете, неудобно использовать на клиенте. Чтобы противостоять таким ситуациям, моя команда создает модель на стороне клиента, в которую мы переводим модель на стороне сервера после десериализации. Мы оборачиваем все это в клиентский api. Как только вы начнете получать более сложные примеры, чем то, что вы показали, этот подход очень полезен, поскольку он защищает клиента от неверных решений / apis модели сервера.
spierce7
Я не согласен с тем, что вы называете «лучшим подходом». Проблема, которую я вижу, заключается в том, что очень часто хочется установить значение в классе данных и никогда не изменять его. Например, преобразование строки в int. Пользовательские геттеры / сеттеры для класса данных не только полезны, но и необходимы; в противном случае у вас останутся POJO-объекты Java-бина, которые ничего не делают, и их поведение + проверка содержится в каком-то другом классе.
Абхиджит Саркар
Я сказал: «Это, вероятно, лучший подход для большинства случаев». В большинстве случаев, если не возникают определенные обстоятельства, разработчики должны иметь четкое разделение между своей моделью и алгоритмом / бизнес-логикой, где результирующая модель из их алгоритма четко представляет различные состояния возможных результатов. Kotlin отлично подходит для этого с закрытыми классами и классами данных. В вашем примере parsing a string into an intвы явно разрешаете бизнес-логику синтаксического анализа и обработки ошибок нечисловых строк в своем классе модели ...
spierce7
... Практика размывания границ между моделью и бизнес-логикой всегда приводит к менее удобному в сопровождении коду, и я бы сказал, что это анти-шаблон. Вероятно, 99% создаваемых мною классов данных являются неизменяемыми или не имеют сеттеров. Я думаю, вам действительно понравится потратить некоторое время, чтобы прочитать о преимуществах неизменности моделей вашей команды. С неизменяемыми моделями я могу гарантировать, что мои модели не будут случайно изменены в каком-либо другом случайном месте кода, что уменьшает побочные эффекты и, опять же, приводит к поддерживаемому коду. т.е. Котлин не отделился Listи MutableListбез причины.
spierce7 08
31
Вы можете попробовать что-то вроде этого:
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
В классе данных вы должны пометить параметры основного конструктора с помощью valили var.
Я задаю значение _valueдля valueтого , чтобы использовать нужное имя для свойства.
Я определил специальный метод доступа для свойства с описанной вами логикой.
У меня ошибка в IDE: «Инициализатор здесь не разрешен, так как у этого свойства нет поля поддержки»
Ченг
6
Ответ зависит от того, какие возможности вы фактически используете data. @EPadron упомянул отличный трюк (улучшенная версия):
dataclassTest(privateval _value: Int) {
val value: Intget() = if (_value < 0) 0else _value
}
Это будет работает , как и ожидалось, е имеет одно поле, один поглотитель, справа equals, hashcodeи component1. Загвоздка в том, что toStringи copyони странные:
Я знаю, что это старый вопрос, но, похоже, никто не упоминал о возможности сделать значение приватным и написать собственный получатель следующим образом:
dataclassTest(privateval value: Int) {
fungetValue(): Int = if (value < 0) 0else value
}
Это должно быть совершенно верно, поскольку Kotlin не будет генерировать получатель по умолчанию для частного поля.
Но в остальном я определенно согласен со spierce7 в том, что классы данных предназначены для хранения данных, и вам следует избегать жесткого кодирования там «бизнес-логики».
Я согласен с вашим решением, но чем в коде, вам придется называть его так, val value = test.getValue() а не как другие геттеры val value = test.value
гори
Да. Это правильно. Это немного отличается, если вы вызываете его с Java, поскольку он всегда там.getValue()
bio007
1
Я видел ваш ответ, я согласен с тем, что классы данных предназначены только для хранения данных, но иногда нам нужно что-то из них сделать.
Вот что я делаю со своим классом данных: я изменил некоторые свойства с val на var и переопределил их в конструкторе.
вот так:
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)
}
}
Делать поля изменяемыми только для того, чтобы их можно было изменять во время инициализации, - плохая практика. Было бы лучше сделать конструктор закрытым, а затем создать функцию, которая действует как конструктор (т.е. fun Recording(...): Recording { ... }). Также, возможно, класс данных - это не то, что вам нужно, поскольку с классами без данных вы можете отделить свои свойства от параметров конструктора. Лучше четко указать свои намерения по изменчивости в определении вашего класса. Если эти поля в любом случае также могут быть изменяемыми, то с классом данных все в порядке, но почти все мои классы данных неизменяемы.
spierce7
@ spierce7 неужели так плохо заслужить голосование против? В любом случае, это решение мне подходит, оно не требует большого количества кода и сохраняет хэш и равнозначно.
Симу
0
Кажется, это один (среди прочего) досадный недостаток Kotlin.
Похоже, что единственное разумное решение, которое полностью сохраняет обратную совместимость класса, - это преобразовать его в обычный класс (а не класс «данные») и реализовать вручную (с помощью IDE) методы: hashCode ( ), equals (), toString (), copy () и 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)
}
}
Во-первых, обратите внимание, что _valueэто varне так val, но, с другой стороны, поскольку он является частным и классы данных не могут быть унаследованы от него, довольно легко убедиться, что он не изменен внутри класса.
Во-вторых, toString()дает несколько иной результат, чем если бы он _valueбыл назван value, но он согласован и TestData(0).toString() == TestData(-1).toString().
Ответы:
Потратив почти год на написание Kotlin ежедневно, я обнаружил, что попытки переопределить классы данных, подобные этому, - плохая практика. Есть 3 действительных подхода к этому, и после того, как я их представлю, я объясню, почему подход, предложенный другими ответами, плох.
Имейте свою бизнес-логику, которая создает
data class
изменение значения на 0 или больше, прежде чем вызывать конструктор с неверным значением. Вероятно, это лучший подход для большинства случаев.Не используйте
data class
. Используйте обычныйclass
и ваш IDE генерироватьequals
иhashCode
методы для вас (или нет, если они не нужны). Да, вам придется повторно сгенерировать его, если какое-либо из свойств объекта изменится, но вы остаетесь с полным контролем над объектом.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() } }
Создайте дополнительное безопасное свойство объекта, которое делает то, что вы хотите, вместо того, чтобы иметь частное значение, которое эффективно переопределяет.
data class Test(val value: Int) { val safeValue: Int get() = if (value < 0) 0 else value }
Плохой подход, который предлагают другие ответы:
data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
Проблема с этим подходом заключается в том, что классы данных на самом деле не предназначены для такого изменения данных. Они действительно предназначены только для хранения данных. Переопределение получателя для такого класса данных означало бы это,
Test(0)
аTest(-1)
неequal
друг друга, и у них были бы разныеhashCode
s, но когда вы вызываете.value
, они будут иметь тот же результат. Это непоследовательно, и хотя это может сработать для вас, другие люди в вашей команде, которые видят, что это класс данных, могут случайно злоупотребить им, не понимая, как вы его изменили / заставили его работать не так, как ожидалось (т. Е. Этот подход не t работать правильно в аMap
или аSet
).источник
data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }
, и считаю, что это неплохо для моего случая, tbh. Что Вы думаете об этом? (были и другие поля, и поэтому я считаю, что для меня не имело смысла воссоздавать эту вложенную структуру json в моем коде)parsing a string into an int
вы явно разрешаете бизнес-логику синтаксического анализа и обработки ошибок нечисловых строк в своем классе модели ...List
иMutableList
без причины.Вы можете попробовать что-то вроде этого:
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
В классе данных вы должны пометить параметры основного конструктора с помощью
val
илиvar
.Я задаю значение
_value
дляvalue
того , чтобы использовать нужное имя для свойства.Я определил специальный метод доступа для свойства с описанной вами логикой.
источник
Ответ зависит от того, какие возможности вы фактически используете
data
. @EPadron упомянул отличный трюк (улучшенная версия):data class Test(private val _value: Int) { val value: Int get() = if (_value < 0) 0 else _value }
Это будет работает , как и ожидалось, е имеет одно поле, один поглотитель, справа
equals
,hashcode
иcomponent1
. Загвоздка в том, чтоtoString
иcopy
они странные:println(Test(1)) // prints: Test(_value=1) Test(1).copy(_value = 5) // <- weird naming
Чтобы решить проблему,
toString
вы можете переопределить ее вручную. Я не знаю, как исправить имя параметра, но не использовать егоdata
вообще.источник
Я знаю, что это старый вопрос, но, похоже, никто не упоминал о возможности сделать значение приватным и написать собственный получатель следующим образом:
data class Test(private val value: Int) { fun getValue(): Int = if (value < 0) 0 else value }
Это должно быть совершенно верно, поскольку Kotlin не будет генерировать получатель по умолчанию для частного поля.
Но в остальном я определенно согласен со spierce7 в том, что классы данных предназначены для хранения данных, и вам следует избегать жесткого кодирования там «бизнес-логики».
источник
val value = test.getValue()
а не как другие геттерыval value = test.value
.getValue()
Я видел ваш ответ, я согласен с тем, что классы данных предназначены только для хранения данных, но иногда нам нужно что-то из них сделать.
Вот что я делаю со своим классом данных: я изменил некоторые свойства с val на var и переопределил их в конструкторе.
вот так:
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 { ... }
). Также, возможно, класс данных - это не то, что вам нужно, поскольку с классами без данных вы можете отделить свои свойства от параметров конструктора. Лучше четко указать свои намерения по изменчивости в определении вашего класса. Если эти поля в любом случае также могут быть изменяемыми, то с классом данных все в порядке, но почти все мои классы данных неизменяемы.Кажется, это один (среди прочего) досадный недостаток Kotlin.
Похоже, что единственное разумное решение, которое полностью сохраняет обратную совместимость класса, - это преобразовать его в обычный класс (а не класс «данные») и реализовать вручную (с помощью IDE) методы: hashCode ( ), equals (), toString (), copy () и 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) } }
источник
Я обнаружил, что следующее является лучшим подходом для достижения того, что вам нужно, без поломок
equals
и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())
Однако,
Во-первых, обратите внимание, что
_value
этоvar
не такval
, но, с другой стороны, поскольку он является частным и классы данных не могут быть унаследованы от него, довольно легко убедиться, что он не изменен внутри класса.Во-вторых,
toString()
дает несколько иной результат, чем если бы он_value
был названvalue
, но он согласован иTestData(0).toString() == TestData(-1).toString()
.источник
_value
модифицируется в блоке инициализацииequals
иhashCode
не нарушается.