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

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

data class Test(val value: Int)

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

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

9 ответов

Решение

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

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

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

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

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

Вот что я делаю со своим классом данных: я изменил некоторые свойства с 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)
    }
}

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

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

Кажется, старый, но интересный вопрос. Просто хочу внести свой вклад:

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

Теперь вы можете переопределить getValue и по-прежнему использовать component1 ().

Это, кажется, один (среди прочих) раздражающих недостатков Kotlin.

Кажется, что единственное разумное решение, которое полностью сохраняет обратную совместимость класса, - это преобразовать его в обычный класс (не класс "данных") и реализовать вручную (с помощью IDE) методы: hashCode(), равно (), 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)
    }

}

Вы можете следовать шаблону Builder для этого, я думаю, что это было бы намного лучше.

Вот пример:

      data class Test(
    // Fields:
    val email: String,
    val password: String
) {

    // Builder(User):
    class Builder(private val email: String) {

        // Fields:
        private lateinit var password: String

        // Methods:
        fun setPassword(password: String): Builder {
            // Some operation like encrypting
            this.password = password
            // Returning:
            return this
        }

        fun build(): Test = Test(email, password)
    }
}
Другие вопросы по тегам