属性 include/exclude Kotlin 数据 类
Property include/exclude on Kotlin data classes
假设我只想在生成的 equals 和 hashCode 实现中包含一个或两个字段(或者可能排除一个或多个字段)。对于简单的class,例如:
data class Person(val id: String, val name: String)
Groovy 有这个:
@EqualsAndHashCode(includes = 'id')
龙目岛有这个:
@EqualsAndHashCode(of = "id")
在 Kotlin 中执行此操作的惯用方法是什么?
到目前为止我的方法
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
是可变的,而且额外的构造函数定义很难看。
这里有一个有点创意的方法:
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])"
}
我也不知道 "the 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()
}
}
难道没有"better"方法吗?
我用过这种方法。
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)
}
这建立在@bashor 的方法之上,并使用私有主构造函数和 public 辅助构造函数。遗憾的是,equals 要忽略的 属性 不能是 val,但可以隐藏 setter,因此从外部角度来看结果是等价的。
data class ExampleDataClass private constructor(val important: String) {
var notSoImportant: String = ""
private set
constructor(important: String, notSoImportant: String) : this(important) {
this.notSoImportant = notSoImportant
}
}
可重复使用的解决方案: 有一个简单的方法来 select 在 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()
}
可以通过额外的计算来权衡存储在 class 中的支持字段 on-the-fly:
private val stem get() = Stem(this, { id })
由于 Stem
接受任何函数,您可以自由指定相等性的计算方式。要考虑多个字段,只需为每个字段添加一个 lambda 表达式(可变参数):
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
class 可以进一步扩展,包括自动生成 toString()
等
此方法可能适用于 属性 排除:
class SkipProperty<T>(val property: T) {
override fun equals(other: Any?) = true
override fun hashCode() = 0
}
SkipProperty.equals
只是 returns true,这会导致嵌入的 property
在父对象的 equals
中被跳过。
data class Person(
val id: String,
val name: SkipProperty<String>
)
您可以创建一个 annotation,表示 属性 的排除,如 @ExcludeToString
或通过定义枚举 @ToString(Type.EXCLUDE)
参数。
然后使用格式化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
更简单,更快,看看那里,或者进入 Kotlin 文档。
https://discuss.kotlinlang.org/t/ignoring-certain-properties-when-generating-equals-hashcode-etc/2715/2
仅考虑主构造函数中的字段来构建自动访问方法,如 equals 等。不要把无意义的放在外面。
考虑以下用于实施 equals/hashcode 的通用方法。由于使用了内联和 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
}
}
有了这个,您可以轻松地 implement/override 任何 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
}
}
如果您不想触及数据class。
,这是另一种骇人听闻的方法
您可以重用数据 class 中的整个 equals()
,同时排除某些字段。
仅 copy()
class 具有排除字段固定值的元素:
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 实现中包含一个或两个字段(或者可能排除一个或多个字段)。对于简单的class,例如:
data class Person(val id: String, val name: String)
Groovy 有这个:
@EqualsAndHashCode(includes = 'id')
龙目岛有这个:
@EqualsAndHashCode(of = "id")
在 Kotlin 中执行此操作的惯用方法是什么?
到目前为止我的方法
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
是可变的,而且额外的构造函数定义很难看。
这里有一个有点创意的方法:
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])"
}
我也不知道 "the 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()
}
}
难道没有"better"方法吗?
我用过这种方法。
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)
}
这建立在@bashor 的方法之上,并使用私有主构造函数和 public 辅助构造函数。遗憾的是,equals 要忽略的 属性 不能是 val,但可以隐藏 setter,因此从外部角度来看结果是等价的。
data class ExampleDataClass private constructor(val important: String) {
var notSoImportant: String = ""
private set
constructor(important: String, notSoImportant: String) : this(important) {
this.notSoImportant = notSoImportant
}
}
可重复使用的解决方案: 有一个简单的方法来 select 在 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()
}
可以通过额外的计算来权衡存储在 class 中的支持字段 on-the-fly:
private val stem get() = Stem(this, { id })
由于 Stem
接受任何函数,您可以自由指定相等性的计算方式。要考虑多个字段,只需为每个字段添加一个 lambda 表达式(可变参数):
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
class 可以进一步扩展,包括自动生成 toString()
等
此方法可能适用于 属性 排除:
class SkipProperty<T>(val property: T) {
override fun equals(other: Any?) = true
override fun hashCode() = 0
}
SkipProperty.equals
只是 returns true,这会导致嵌入的 property
在父对象的 equals
中被跳过。
data class Person(
val id: String,
val name: SkipProperty<String>
)
您可以创建一个 annotation,表示 属性 的排除,如 @ExcludeToString
或通过定义枚举 @ToString(Type.EXCLUDE)
参数。
然后使用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
更简单,更快,看看那里,或者进入 Kotlin 文档。 https://discuss.kotlinlang.org/t/ignoring-certain-properties-when-generating-equals-hashcode-etc/2715/2 仅考虑主构造函数中的字段来构建自动访问方法,如 equals 等。不要把无意义的放在外面。
考虑以下用于实施 equals/hashcode 的通用方法。由于使用了内联和 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
}
}
有了这个,您可以轻松地 implement/override 任何 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
}
}
如果您不想触及数据class。
,这是另一种骇人听闻的方法
您可以重用数据 class 中的整个 equals()
,同时排除某些字段。
仅 copy()
class 具有排除字段固定值的元素:
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