Замінити отримання для класу даних Kotlin


98

Враховуючи наступний клас Котліна:

data class Test(val value: Int)

Як би я перевизначив Intгеттер, щоб він повертав 0, якщо значення від’ємне?

Якщо це неможливо, якими методами можна досягти відповідного результату?


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один одного і будуть мати різні hashCodeс, але коли ви подзвонили .value, вони будуть мати той же результат. Це суперечливо, і хоча це може спрацювати для вас, інші люди у вашій команді, які бачать, що це клас даних, можуть випадково використовувати його, не усвідомлюючи, як ви його змінили / змусили його працювати не так, як очікувалося (тобто такий підхід не міг би т правильно працювати в а 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. Як тільки ви починаєте отримувати приклади, які є більш складними, ніж те, що ви показали, цей підхід дуже корисний, оскільки захищає клієнта від неправильних рішень моделі сервера / API.
spierce7

Я не погоджуюся з тим, що, на вашу думку, є "найкращим підходом". Проблема, яку я бачу, полягає в тому, що дуже часто хочеться встановити значення в класі даних і ніколи не змінювати його. Наприклад, аналіз рядка на int. Спеціальні геттери / сеттери для класу даних не тільки корисні, але й необхідні; в іншому випадку вам залишаються POJO-компоненти Java-компонентів, які нічого не роблять, а їх поведінка + перевірка міститься в якомусь іншому класі.
Абхіджіт Саркар

Я сказав: "Це, мабуть, найкращий підхід для більшості випадків". У більшості випадків, якщо не виникають певні обставини, розробники повинні чітко відокремлювати свою модель від алгоритму / бізнес-логіки, де отримана модель від їх алгоритму чітко представляє різні стани можливих результатів. Kotlin для цього є фантастичним, із закритими класами та класами даних. Для вашого прикладу parsing a string into an int, ви чітко дозволяєте бізнес-логіку синтаксичного аналізу та обробки помилок нечислових рядків у своєму класі моделі ...
spierce7

... Практика розмивання межі між моделлю та бізнес-логікою завжди веде до менш ремонтопридатного коду, і я б стверджував, що це шаблон. Можливо, 99% класів даних, які я створюю, є незмінними / відсутніми установщиками. Думаю, вам справді сподобалося б витратити трохи часу, щоб прочитати про переваги своєї команди, зберігаючи свої моделі незмінними. З незмінними моделями я можу гарантувати, що мої моделі випадково не модифікуються в іншому випадковому місці коду, що зменшує побічні ефекти, і знову ж таки, призводить до коду, який можна підтримувати. тобто Котлін не відокремлював ListіMutableList без причини.
spierce7

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того , щоб використовувати потрібне ім'я для властивості.

  • Я визначив спеціальний доступ для властивості за логікою, яку ви описали.


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


Я погоджуюся з вашим рішенням, але, ніж у коді, вам доведеться називати його так, val value = test.getValue() а не як інші геттери val value = test.value
gori

Так. Це правильно. Трохи інакше, якщо зателефонувати з 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)
    }
}

Змінювати поля просто так, щоб ви могли їх модифікувати під час ініціалізації, є поганою практикою. Було б краще зробити конструктор приватним, а потім створити функцію, яка виступає в ролі конструктора (тобто fun Recording(...): Recording { ... }). Також, можливо, клас даних - це не те, що вам потрібно, оскільки за допомогою класів, що не належать до даних, ви можете відокремити свої властивості від параметрів конструктора. Краще чітко вказати свої наміри щодо змінності у визначенні класу. Якщо ці поля в будь-якому випадку також можуть бути змінними, то клас даних чудовий, але майже всі мої класи даних незмінні.
spierce7

@ spierce7 чи справді так погано заслуговувати на голосування проти? У будь-якому випадку, це рішення мене влаштовує, воно не вимагає великої кількості кодування, і воно зберігає хеш і рівнозначні.
Сімоу

0

Здається, це один (серед інших) прикрих недоліків Котліна.

Здається, єдиним розумним рішенням, яке повністю зберігає зворотну сумісність класу, є перетворення його в звичайний клас (не клас "даних") та реалізація вручну (за допомогою IDE) методів: hashCode ( ), дорівнює (), toString (), copy () та компонентN ()

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().


@ spierce7 Ні, це не так. _valueмодифікується в блоці init equalsі hashCode не порушено.
schatten

Ви можете допомогти мені з цим stackoverflow.com/questions/64015108 / ...
Wishy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.