Переопределить получатель для класса данных Kotlin

99

Учитывая следующий класс Kotlin:

data class Test(val value: Int)

Как мне переопределить Intгеттер, чтобы он возвращал 0, если значение отрицательное?

Если это невозможно, каковы методы достижения подходящего результата?

Spierce7
источник
14
Пожалуйста, подумайте об изменении структуры вашего кода, чтобы отрицательные значения преобразовывались в 0 при создании экземпляра класса, а не в геттере. Если вы переопределите получатель, как описано в ответе ниже, все другие сгенерированные методы, такие как equals (), toString () и доступ к компонентам, по-прежнему будут использовать исходное отрицательное значение, что, вероятно, приведет к неожиданному поведению.
yole

Ответы:

148

Потратив почти год на написание Kotlin ежедневно, я обнаружил, что попытки переопределить классы данных, подобные этому, - плохая практика. Есть 3 действительных подхода к этому, и после того, как я их представлю, я объясню, почему подход, предложенный другими ответами, плох.

  1. Имейте свою бизнес-логику, которая создает data classизменение значения на 0 или больше, прежде чем вызывать конструктор с неверным значением. Вероятно, это лучший подход для большинства случаев.

  2. Не используйте 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()
      }
    }
    
  3. Создайте дополнительное безопасное свойство объекта, которое делает то, что вы хотите, вместо того, чтобы иметь частное значение, которое эффективно переопределяет.

    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друг друга, и у них были бы разные hashCodes, но когда вы вызываете .value, они будут иметь тот же результат. Это непоследовательно, и хотя это может сработать для вас, другие люди в вашей команде, которые видят, что это класс данных, могут случайно злоупотребить им, не понимая, как вы его изменили / заставили его работать не так, как ожидалось (т. Е. Этот подход не t работать правильно в а Mapили а Set).

Spierce7
источник
как насчет классов данных, используемых для сериализации / десериализации, выравнивания вложенной структуры? Например, я только что написал 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

Вы можете попробовать что-то вроде этого:

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того , чтобы использовать нужное имя для свойства.

  • Я определил специальный метод доступа для свойства с описанной вами логикой.

EPadronU
источник
2
У меня ошибка в IDE: «Инициализатор здесь не разрешен, так как у этого свойства нет поля поддержки»
Ченг
6

Ответ зависит от того, какие возможности вы фактически используете 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вообще.

воддан
источник
2

Я знаю, что это старый вопрос, но, похоже, никто не упоминал о возможности сделать значение приватным и написать собственный получатель следующим образом:

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

Это должно быть совершенно верно, поскольку Kotlin не будет генерировать получатель по умолчанию для частного поля.

Но в остальном я определенно согласен со spierce7 в том, что классы данных предназначены для хранения данных, и вам следует избегать жесткого кодирования там «бизнес-логики».

bio007
источник
Я согласен с вашим решением, но чем в коде, вам придется называть его так, val value = test.getValue() а не как другие геттеры val value = test.value
гори
Да. Это правильно. Это немного отличается, если вы вызываете его с Java, поскольку он всегда там.getValue()
bio007
1

Я видел ваш ответ, я согласен с тем, что классы данных предназначены только для хранения данных, но иногда нам нужно что-то из них сделать.

Вот что я делаю со своим классом данных: я изменил некоторые свойства с 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)
    }
}
Simou
источник
Делать поля изменяемыми только для того, чтобы их можно было изменять во время инициализации, - плохая практика. Было бы лучше сделать конструктор закрытым, а затем создать функцию, которая действует как конструктор (т.е. fun Recording(...): Recording { ... }). Также, возможно, класс данных - это не то, что вам нужно, поскольку с классами без данных вы можете отделить свои свойства от параметров конструктора. Лучше четко указать свои намерения по изменчивости в определении вашего класса. Если эти поля в любом случае также могут быть изменяемыми, то с классом данных все в порядке, но почти все мои классы данных неизменяемы.
spierce7
@ spierce7 неужели так плохо заслужить голосование против? В любом случае, это решение мне подходит, оно не требует большого количества кода и сохраняет хэш и равнозначно.
Симу
0

Кажется, это один (среди прочего) досадный недостаток 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)
    }

}
Ашер Стерн
источник
1
Не уверен, что назвал бы это недостатком. Это просто ограничение функции класса данных, которую Java не предлагает.
spierce7
0

Я обнаружил, что следующее является лучшим подходом для достижения того, что вам нужно, без поломок 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().

Schatten
источник
@ spierce7 Нет, это не так. _valueмодифицируется в блоке инициализации equalsи hashCode не нарушается.
schatten
Можете