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

11 ответов

Решение

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

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) 
}

Этот подход может быть подходящим для исключения собственности:

class SkipProperty<T>(val property: T) {
  override fun equals(other: Any?) = true
  override fun hashCode() = 0
}

SkipProperty.equals просто возвращает истину, что приводит к встраиванию property быть пропущенным в equals родительского объекта.

data class Person(
    val id: String, 
    val name: SkipProperty<String>
)

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

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

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

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


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

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

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

Я также не знаю "идиоматического пути" в 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()
    }
}

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

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

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])"
}

Проще, быстрее, посмотрите там или в документации Kotlin. sure-properties-when-generating-equals- https://discuss.kotlinlang.org/t/ignoring-hashcode-etc/2715/2 Учитываются только поля внутри основного конструктора для создания методов автоматического доступа, таких как equals и т. д. на. Держите бессмысленные снаружи.

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

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

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's 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() и т.п.

Вот еще один хакерский подход, если вы не хотите трогать класс данных.
Вы можете повторно использовать весь equals()из классов данных, исключая некоторые поля.
Просто copy()классы с фиксированными значениями для исключенных полей:

      data class Person(val id: String,
                  val name: String)
fun main() {
    val person1 = Person("1", "John")
    val person2 = Person("2", "John")
    println("Full equals: ${person1 == person2}")
    println("equals without id: ${person1.copy(id = "") == person2.copy(id = "")}")
   
}

Выход:

      Full equals: false
equals without id: true

Рассмотрим следующий общий подход к реализации equals / hashcode. Приведенный ниже код не должен влиять на производительность из-за использования классов значений inlining и kotlin:

      @file:Suppress("EXPERIMENTAL_FEATURE_WARNING")

package org.beatkit.common

import kotlin.jvm.JvmInline

@Suppress("NOTHING_TO_INLINE")
@JvmInline
value class HashCode(val value: Int = 0) {
    inline fun combineHash(hash: Int): HashCode = HashCode(31 * value + hash)
    inline fun combine(obj: Any?): HashCode = combineHash(obj.hashCode())
}

@Suppress("NOTHING_TO_INLINE")
@JvmInline
value class Equals(val value: Boolean = true) {
    inline fun combineEquals(equalsImpl: () -> Boolean): Equals = if (!value) this else Equals(equalsImpl())
    inline fun <A : Any> combine(lhs: A?, rhs: A?): Equals = combineEquals { lhs == rhs }
}

@Suppress("NOTHING_TO_INLINE")
object Objects {
    inline fun hashCode(builder: HashCode.() -> HashCode): Int = builder(HashCode()).value

    inline fun hashCode(vararg objects: Any?): Int = hashCode {
        var hash = this
        objects.forEach {
            hash = hash.combine(it)
        }
        hash
    }

    inline fun hashCode(vararg hashes: Int): Int = hashCode {
        var hash = this
        hashes.forEach {
            hash = hash.combineHash(it)
        }
        hash
    }

    inline fun <T : Any> equals(
        lhs: T,
        rhs: Any?,
        allowSubclasses: Boolean = false,
        builder: Equals.(T, T) -> Equals
    ): Boolean {
        if (rhs == null) return false
        if (lhs === rhs) return true
        if (allowSubclasses) {
            if (!lhs::class.isInstance(rhs)) return false
        } else {
            if (lhs::class != rhs::class) return false
        }
        @Suppress("unchecked_cast")
        return builder(Equals(), lhs, rhs as T).value
    }
}

Имея это на месте, вы можете легко реализовать / переопределить любую реализацию equals / hashcode единообразным образом:

      data class Foo(val title: String, val bytes: ByteArray, val ignore: Long) {
    override fun equals(other: Any?): Boolean {
        return Objects.equals(this, other) { lhs, rhs ->
            this.combine(lhs.title, rhs.title)
                .combineEquals { lhs.bytes contentEquals rhs.bytes }
            // ignore the third field for equals
        }
    }

    override fun hashCode(): Int {
        return Objects.hashCode(title, bytes) // ignore the third field for hashcode
    } 
}

Вы можете создать аннотацию, которая представляет исключение свойства как@ExcludeToString или с @ToString(Type.EXCLUDE) параметры, определяя enum.

А затем, используя формат отражения, значениеgetToString().

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExcludeToString

data class Test(
        var a: String = "Test A",
        @ExcludeToString var b: String = "Test B"
) {
    override fun toString(): String {
        return ExcludeToStringUtils.getToString(this)
    }
}

object ExcludeToStringUtils {

    fun getToString(obj: Any): String {
        val toString = LinkedList<String>()
        getFieldsNotExludeToString(obj).forEach { prop ->
            prop.isAccessible = true
            toString += "${prop.name}=" + prop.get(obj)?.toString()?.trim()
        }
        return "${obj.javaClass.simpleName}=[${toString.joinToString(", ")}]"
    }

    private fun getFieldsNotExludeToString(obj: Any): List<Field> {
        val declaredFields = obj::class.java.declaredFields
        return declaredFields.filterNot { field ->
            isFieldWithExludeToString(field)
        }
    }

    private fun isFieldWithExludeToString(field: Field): Boolean {
        field.annotations.forEach {
            if (it.annotationClass == ExcludeToString::class) {
                return true
            }
        }
        return false
    }

}

GL

Суть

Другие вопросы по тегам