Свойство включает/исключает классы данных Kotlin

Предположим, что я хочу, чтобы одно или два поля включались в сгенерированные выражения equals и hashCode (или, возможно, исключали одно или несколько полей). Для простого класса, например:

data class Person(val id: String, val name: String)

Groovy имеет следующее значение:

@EqualsAndHashCode(includes = 'id')

Ломбок имеет следующее:

@EqualsAndHashCode(of = "id")

Каков идиоматический способ сделать это в Котлине?

Мой подход до сих пор

data class Person(val id: String) {
   // at least we can guarantee it is present at access time
   var name: String by Delegates.notNull()

   constructor(id: String, name: String): this(id) {
      this.name = name
   }
}

Просто чувствует себя не так, хотя... Я действительно не хочу, чтобы name изменялся, а дополнительное определение конструктора было уродливым.

Ответы

Ответ 1

Я использовал этот подход.

data class Person(val id: String, val name: String) {
   override fun equals(other: Person) = EssentialData(this) == EssentialData(other)
   override fun hashCode() = EssentialData(this).hashCode()
   override fun toString() = EssentialData(this).toString().replaceFirst("EssentialData", "Person")
}

private data class EssentialData(val id: String) {
   constructor(person: Person) : this(id = person.id) 
}

Ответ 2

К сожалению, это решение больше не работает, и у меня нет (еще) еще одной хорошей идеи для этого вопроса.


Автогенерируемые функции используют только свойства (параметры с val или var), объявленные в основном конструкторе. Итак, вы можете написать:

data class Person(val id: String, name: String) {
   val name: String = name
}

Из дополнительной информации см. ссылку классы данных.

Ответ 3

Я также не знаю "idomatic way" в Kotlin (1.1), чтобы сделать это...

Я закончил переопределять equals и hashCode:

data class Person(val id: String,
                  val name: String) {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Person

        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

Разве нет "лучшего" способа?

Ответ 4

Вот несколько творческий подход:

data class IncludedArgs(val args: Array<out Any>)

fun includedArgs(vararg args: Any) = IncludedArgs(args)


abstract class Base {
    abstract val included : IncludedArgs

    override fun equals(other: Any?) = when {
        this identityEquals other -> true
        other is Base -> included == other.included
        else -> false
    }

    override fun hashCode() = included.hashCode()

    override fun toString() = included.toString()
}

class Foo(val a: String, val b : String) : Base() {
    override val included = includedArgs(a)
}

fun main(args : Array<String>) {
    val foo1 = Foo("a", "b")
    val foo2 = Foo("a", "B")

    println(foo1 == foo2) //prints "true"
    println(foo1)         //prints "IncludedArgs(args=[a])"
}

Ответ 5

Это основано на подходе @bashor и использует частный первичный и публичный вторичный конструктор. К сожалению, свойство, которое следует игнорировать для равных, не может быть val, но можно скрыть сеттер, поэтому результат эквивалентен с внешней точки зрения.

data class ExampleDataClass private constructor(val important: String) {
  var notSoImportant: String = ""
    private set

  constructor(important: String, notSoImportant: String) : this(important) {
    this.notSoImportant = notSoImportant
  }
}

Ответ 6

Многоразовое решение: чтобы иметь простой способ выбора полей для включения в функции equals() и hashCode(), я написал небольшой помощник под названием "stem" (основные данные, относящиеся к равенству).

Использование простое, а полученный код очень маленький:

class Person(val id: String, val name: String) {
    private val stem = Stem(this, { id })

    override fun equals(other: Any?) = stem.eq(other)
    override fun hashCode() = stem.hc()
}

Можно обменять поле поддержки, сохраненное в классе, на дополнительные вычисления на лету:

    private val stem get() = Stem(this, { id })

Поскольку Stem принимает любую функцию, вы можете указать, как вычисляется равенство. Чтобы рассмотреть более одного поля, просто добавьте одно лямбда-выражение на поле (varargs):

    private val stem = Stem(this, { id }, { name })

Реализация:

class Stem<T : Any>(
        private val thisObj: T,
        private vararg val properties: T.() -> Any?
) {     
    fun eq(other: Any?): Boolean {
        if (thisObj === other)
            return true

        if (thisObj.javaClass != other?.javaClass)
            return false

        // cast is safe, because this is T and other class was checked for equality with T
        @Suppress("UNCHECKED_CAST") 
        other as T

        return properties.all { thisObj.it() == other.it() }
    }

    fun hc(): Int {
        // Fast implementation without collection copies, based on java.util.Arrays.hashCode()
        var result = 1

        for (element in properties) {
            val value = thisObj.element()
            result = 31 * result + (value?.hashCode() ?: 0)
        }

        return result
    }

    @Deprecated("Not accessible; use eq()", ReplaceWith("this.eq(other)"), DeprecationLevel.ERROR)
    override fun equals(other: Any?): Boolean = 
        throw UnsupportedOperationException("Stem.equals() not supported; call eq() instead")

    @Deprecated("Not accessible; use hc()", ReplaceWith("this.hc(other)"), DeprecationLevel.ERROR)
    override fun hashCode(): Int = 
        throw UnsupportedOperationException("Stem.hashCode() not supported; call hc() instead")
}

Если вам интересны последние два метода, их наличие приводит к сбою следующего ошибочного кода во время компиляции:

override fun equals(other: Any?) = stem.equals(other)
override fun hashCode() = stem.hashCode()

Исключением является просто запасной вариант, если эти методы вызываются неявно или через отражение; Можно спорить, если это необходимо.

Конечно, класс Stem может быть дополнительно расширен для включения автоматической генерации toString() и т.д.