【问题标题】:Override getter for Kotlin data class覆盖 Kotlin 数据类的 getter
【发布时间】:2016-11-24 07:55:33
【问题描述】:

给定以下 Kotlin 类:

data class Test(val value: Int)

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

如果这不可能,有哪些技术可以达到合适的结果?

【问题讨论】:

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

标签: kotlin


【解决方案1】:

在每天花费将近一整年的时间编写 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. 在对象上创建一个额外的安全属性来执行您想要的操作,而不是拥有一个被有效覆盖的私有值。

    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
}

这种方法的问题在于data classes 并不是真正用于更改这样的数据。它们实际上只是用于保存数据。像这样覆盖数据类的 getter 意味着 Test(0)Test(-1) 不会彼此 equal 并且会有不同的 hashCodes,但是当你调用 .value 时,它们会得到相同的结果.这是不一致的,虽然它可能对您有用,但您团队中的其他人看到这是一个数据类,可能会在没有意识到您如何更改它/使其无法按预期工作的情况下意外滥用它(即这种方法不会在MapSet 中无法正常工作。

【讨论】:

  • 用于序列化/反序列化、扁平化嵌套结构的数据类怎么样?例如。我刚刚写了data class class(@JsonProperty("iss_position") private val position: Map&lt;String, Double&gt;) { val latitude = position["latitude"]; val longitude = position["longitude"] },我认为这对我的情况非常有利,tbh。你怎么看待这件事? (还有其他字段,因此我认为在我的代码中重新创建嵌套的 json 结构对我来说毫无意义)
  • @Antek 鉴于您没有更改数据,我认为这种方法没有任何问题。我还将提到您这样做的原因是因为您正在发送的服务器端模型不方便在客户端上使用。为了应对这种情况,我的团队创建了一个客户端模型,我们在反序列化后将服务器端模型转换为该模型。我们将所有这些都包装在客户端 api 中。一旦您开始获得比您所展示的更复杂的示例,这种方法非常有用,因为它可以保护客户端免受错误的服务器模型决策 / api 的影响。
  • 我不同意您声称的“最佳方法”。我看到的问题是非常想要在数据类中设置一个值并且从不更改它是很常见的。例如,将字符串解析为 int。数据类上的自定义 getter/setter 不仅有用,而且很有必要;否则,您将得到什么都不做的 Java bean POJO,并且它们的行为 + 验证包含在其他一些类中。
  • 我说的是“这可能是大多数情况下的最佳方法”。在大多数情况下,除非出现某些情况,否则开发人员应该明确区分他们的模型和算法/业务逻辑,其中从他们的算法得到的模型清楚地代表了可能结果的各种状态。 Kotlin 在这方面非常出色,它具有密封类和数据类。对于您的parsing a string into an int 示例,您显然允许在模型类中解析和错误处理非数字字符串的业务逻辑...
  • ...混淆模型和业务逻辑之间界限的做法总是导致代码的可维护性降低,我认为这是一种反模式。我创建的 99% 的数据类可能是不可变的/缺少 setter。我认为您真的很喜欢花一些时间来了解您的团队保持其模型不可变的好处。使用不可变模型,我可以保证我的模型不会在代码中的其他随机位置被意外修改,从而减少副作用,并再次导致代码可维护。即 Kotlin 没有无缘无故地将 ListMutableList 分开。
【解决方案2】:

你可以试试这样的:

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,以便使用所需的属性名称。

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

【讨论】:

  • 我在 IDE 上遇到了一个错误,它说“这里不允许初始化程序,因为这个属性没有支持字段”
【解决方案3】:

答案取决于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

【讨论】:

    【解决方案4】:

    我已经看到你的回答,我同意数据类仅用于保存数据,但有时我们需要从中做出一些东西。

    这是我对数据类所做的事情,我将一些属性从 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 { ... })。也可能数据类不是您想要的,因为使用非数据类,您可以将属性与构造函数参数分开。最好在类定义中明确说明您的可变性意图。如果这些字段也恰好是可变的,那么数据类就可以了,但几乎我所有的数据类都是不可变的。
    【解决方案5】:

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

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

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

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

    【讨论】:

    • 我同意您的解决方案,但与代码相比,您必须像这样 val value = test.getValue() 而不是像其他 getter val value = test.value 那样称呼它
    • 是的。这是正确的。如果你从 Java 中调用它会有点不同,因为它总是.getValue()
    【解决方案6】:

    似乎是一个古老但有趣的问题。 只想贡献一个选项:

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

    现在您可以覆盖 getValue,并且仍然可以让 component1() 正常工作。

    【讨论】:

      【解决方案7】:

      我发现以下方法是在不破坏 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,但另一方面,由于它是私有的并且无法继承数据类,因此很容易确保它没有在类。

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

      【讨论】:

      • @spierce7 不,不是。 _value 正在 init 块中修改,equalshashCode 没有损坏。
      【解决方案8】:

      这似乎是 Kotlin 的一个(以及其他)令人讨厌的缺点。

      似乎唯一合理的解决方案,完全保持类的向后兼容性是将其转换为常规类(不是“数据”类),并手动(在IDE的帮助下)实现方法: hashCode(), 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 不提供此功能。
      猜你喜欢
      • 2018-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-10
      • 2015-07-30
      • 1970-01-01
      相关资源
      最近更新 更多