Свойство включает/исключает классы данных 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()
и т.д.