覆盖Kotlin数据类的getter


94

给定以下Kotlin类:

data class Test(val value: Int)

Int如果值负,我将如何覆盖吸气剂,使其返回0?

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


13
请考虑更改代码的结构,以便在实例化该类时而不是在getter中将负值转换为0。如果按下面的答案中所述覆盖getter,则所有其他生成的方法(例如equals(),toString()和组件访问)仍将使用原始的负值,这很可能导致令人惊讶的行为。
yole

Answers:


138

在每天花费近整整一年的时间写Kotlin之后,我发现尝试像这样重写数据类是一个不好的做法。有3种有效的方法,在介绍它们之后,我将解释为什么其他答案建议的方法不好。

  1. data class在调用具有错误值的构造函数之前,让您的业务逻辑将alter值创建为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. 在对象上创建一个附加的安全属性,该属性执行您想要的操作,而不是具有有效覆盖的私有值。

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

其他答案提示的一种不好的方法:

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

这种方法的问题在于,数据类并不是真的要像这样更改数据。它们实际上只是用于保存数据。重写像这样的数据类,吸气将意味着Test(0)Test(-1)不会equal彼此会有不同的hashCodeS,但是当你打电话.value,他们将有相同的结果。这是不一致的,虽然它可能对您有用,但是团队中其他人看到这是一个数据类时,可能会无意间滥用了它,而没有意识到您如何更改它/使其无法按预期工作(即,这种方法不会不能在a MapSet)中正常工作。


用于序列化/反序列化,展平嵌套结构的数据类又如何呢?例如,我刚刚写过书data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] },我认为这对我的情况相当不错,tbh。你怎么看待这件事?(还有off其他字段,因此我认为在我的代码中重新创建嵌套的json结构对我来说是没有意义的)
Antek

@Antek鉴于您没有更改数据,因此我认为这种方法没有任何问题。我还将提到执行此操作的原因是因为要发送的服务器端模型在客户端上不方便使用。为了应对这种情况,我的团队创建了一个客户端模型,在反序列化之后,我们将其转换为服务器端模型。我们将所有这些都包装在客户端api中。一旦开始获得比显示的示例还要复杂的示例,此方法将非常有用,因为它可以保护客户端免受错误的服务器模型决策/ API的侵害。
spierce7 '18

我不同意您所说的“最佳方法”。我看到的问题是,要在数据类中设置一个值,而不要更改它是常见的。例如,将字符串解析为int。数据类上的自定义获取器/设置器不仅有用,而且很有必要;否则,您将剩下什么都不做的Java bean POJO,它们的行为+验证包含在其他一些类中。
Abhijit Sarkar

我所说的是“这在大多数情况下可能是最好的方法”。在大多数情况下,除非出现某些情况,否则开发人员应在模型与算法/业务逻辑之间明确区分,其中算法得出的模型清楚地表示了可能结果的各种状态。Kotlin拥有密封类和数据类,对此非常棒。对于的示例parsing a string into an int,您显然允许将解析和错误处理非数字字符串的业务逻辑引入模型类中……
spierce7 '19

...弄混模型和业务逻辑之间的界限的做法总是导致代码的可维护性较差,我认为这是一种反模式。我创建的数据类中大概有99%是不可变的/缺少二传手。我认为您真的很乐意花一些时间来阅读您的团队保持模型不变的好处。使用不可变模型,我可以保证我的模型不会在代码中的其他随机位置被意外修改,这减少了副作用,并再次导致了代码的可维护性。即,科特林没有分手List,也没有MutableList任何理由。
spierce7 '19

31

您可以尝试这样的事情:

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
  • 在数据类中,必须使用val或标记主要构造函数的参数var

  • 我正在分配_valueto 的值,value以便为属性使用所需的名称。

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


1
我在IDE上收到错误消息,提示“此处不允许使用Initializer,因为此属性没有后备字段”
Cheng

6

答案取决于您实际使用的功能data。@EPadron提到了一个漂亮的技巧(改进版):

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

这将正常工作,EI它有一个领域,一个消气吧equalshashcodecomponent1。美中不足的是,toStringcopy怪异:

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

要解决该问题,toString可以手动重新定义。我知道没有办法修复参数命名,但根本不使用data


2

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

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

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

但除此之外,我绝对同意spierce7的观点,即数据类用于保存数据,因此您应避免在其中硬编码“业务”逻辑。


我同意您的解决方案,但与在代码中相比,您必须像这样val value = test.getValue() 而不是像其他获取方法 那样来称呼它 val value = test.value
gori

是。没错 如果从Java调用它,则有点不同,因为它总是存在.getValue()
bio007

1

我已经看到了您的答案,我同意数据类仅用于保存数据,但是有时我们需要从中获取一些东西。

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

仅使字段可变以使您可以在初始化期间对其进行修改是一种不好的做法。最好将构造函数设为私有,然后创建一个充当构造函数的函数(即fun Recording(...): Recording { ... })。另外,也许数据类不是您想要的,因为使用非数据类可以将属性与构造函数参数分开。最好在类定义中明确您的可变性意图。如果这些字段也无论如何都是可变的,那么数据类就可以了,但是我几乎所有的数据类都是不可变的。
spierce7

@ spierce7真的应该被否决吗?无论如何,此解决方案非常适合我,它不需要太多的编码,并且可以保持哈希值不变。
Simou

0

这似乎是Kotlin令人讨厌的缺点之一。

看来,完全保持该类的向后兼容性的唯一合理的解决方案是将其转换为常规类(而不是“数据”类),并手动实现(借助IDE)以下方法: ),equals(),toString(),copy()和componentN()

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

}

不知道我会称其为缺点。它仅仅是数据类功能的限制,而不是Java提供的功能。
spierce7

0

我发现以下是实现您所需要的而不中断equals和的最佳方法hashCode

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,但是另一方面,由于它是私有的并且不能继承数据类,因此很容易确保不在类中对其进行修改。

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


@ spierce7不,不是。_value在init块中被修改,equals并且hashCode 没有损坏。
schatten
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.