重写 Kotlin 数据类的 getter

考虑到以下 Kotlin 阶级:

data class Test(val value: Int)

如何覆盖 Int getter 以便在值为负时返回0?

如果这是不可能的,有什么技术可以达到一个合适的结果?

111650 次浏览

你可以试试这样:

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
  • 在数据类中,必须用 valvar标记主构造函数的参数。

  • 我将 _value的值赋给 value,以便为属性使用所需的名称。

  • 我使用您描述的逻辑为该属性定义了一个自定义访问器。

答案取决于你实际上使用了 data提供的哪些功能。@EPadron 提到了一个巧妙的技巧(改进版) :

data class Test(private val _value: Int) {
val value: Int
get() = if (_value < 0) 0 else _value
}

这将按预期工作,例如它有 字段,一个读取器,右 equalshashcodecomponent1。问题是 toStringcopy都很奇怪:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

为了解决 toString的问题,你可以用手重新定义它。我知道没有办法修复参数命名,但不使用 data在所有。

这似乎是 Kotlin 令人恼火的缺点之一。

看起来唯一合理的解决方案,完全保持类的向下兼容,是把它转换成一个常规类(而不是一个“数据”类) ,并且手工实现(借助 IDE)方法: hashCode () ,equals () ,toString () ,copy ()和 Component entN ()

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


}

在花了几乎整整一年的时间编写 Kotlin 日报之后,我发现试图像这样覆盖数据类是一种糟糕的做法。有3种有效的方法可以解决这个问题,在我介绍了它们之后,我将解释为什么其他答案建议的方法是不好的。

  1. 在调用带有坏值的构造函数之前,让创建 data class的业务逻辑将值改为0或更大。对于大多数情况,这可能是最好的方法。

  2. 不要用 data class。使用常规的 class并让 IDE 为您生成 equalshashCode方法(如果不需要,也可以不生成)。是的,如果对象上的任何属性发生了更改,那么您必须重新生成它,但是您可以完全控制对象。

    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. Create an additional safe property on the object that does what you want instead of having a private value that's effectively overriden.

    data class Test(val value: Int) {
    val safeValue: Int
    get() = if (value < 0) 0 else value
    }
    

A bad approach that other answers are suggesting:

data class Test(private val _value: Int) {
val value: Int
get() = if (_value < 0) 0 else _value
}

这种方法的问题在于,数据类别实际上并不是用于像这样改变数据的。它们实际上只是用来保存数据的。重写这样一个数据类的 getter 将意味着 Test(0)Test(-1)不会相互为 equal,它们会有不同的 hashCode,但是当您调用 .value时,它们会有相同的结果。这是不一致的,虽然它可能对你有用,但是你团队中的其他人看到这是一个数据类,可能会意外地误用它,而没有意识到你是如何改变它/使它不能像预期的那样工作(即这种方法在 MapSet中不能正确工作)。

我知道这是一个古老的问题,但似乎没有人提到将价值私有化和编写定制 getter 的可能性:

data class Test(private val value: Int) {
fun getValue(): Int = if (value < 0) 0 else value
}

这应该是完全有效的,因为 Kotlin 不会为私有字段生成默认 getter。

但是,除此之外,我完全同意 Spierce7的观点,即数据类是用来保存数据的,您应该避免在那里硬编码“业务”逻辑。

我发现以下是在不破坏 equalshashCode的情况下达到你所需要的最佳方法:

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

但是,

首先,请注意,_valuevar,而不是 val,但是另一方面,由于它是私有的并且数据类不能从中继承,所以确保它不在类中被修改是相当容易的。

其次,如果将 _value命名为 value,那么 toString()产生的结果会略有不同,但是它是一致的,而且是 TestData(0).toString() == TestData(-1).toString()

我已经看到了你的答案,我同意数据类只是用来保存数据的,但是有时候我们需要从数据类中创造一些东西。

下面是我对我的数据类所做的,我将一些属性从 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)
}
}

这似乎是个古老但有趣的问题。 只是想提供一个选择:

data class Test(@JvmField val value: Int){
fun getValue() = if(value<0) 0 else value
}

现在,您可以覆盖 getValue,并且仍然可以使 Component ent1()工作。

你可以跟随 建造者模式我认为这会更好。

这里有一个例子:

data class Test(
// Fields:
val email: String,
val password: String
) {


// Builder(User):
class Builder(private val email: String) {


// Fields:
private lateinit var password: String


// Methods:
fun setPassword(password: String): Builder {
// Some operation like encrypting
this.password = password
// Returning:
return this
}


fun build(): Test = Test(email, password)
}
}