【问题标题】:Overwriting the default case class constructor (Scala)覆盖默认案例类构造函数(Scala)
【发布时间】:2017-11-10 17:30:25
【问题描述】:

如果我有一个案例类:

case class NonNegativeInt(i: Int)

如果参数为负,则将字段i 设置为0。所以我不能只使用案例类提供的默认构造函数。如果我在伴随对象(或任何地方)中定义一个 apply() 方法:

def apply(n: Int) = new NonNegativeInt(Math.max(0, n))

显然它们具有相同的签名。是否有一种实用的方法/模式来处理字段上的约束?

【问题讨论】:

  • 我不明白您为什么不希望 NonNegativeInt(-100) 返回 NonNegativeInt(0)。您的案例类名称本身非常清楚,它是非负数,所以 apply() 对我来说似乎很好。你是说你想调用SomeClass.someMethodThatReturnsNonNegative(int: Int): NonoNegativeInt

标签: scala case-class


【解决方案1】:
case class NonNegativeInt(i: Int)

如果你不能使用 apply 就给它命名。

object NonNegativeInt {
  def fromInt(i: Int): NonNegativeInt = NonNegativeInt(Math.max(0, i)
}

如果你愿意,你可以变得更花哨,使用Refinedshapeless 对正整数进行类型检查文字常量和编译时间限制,通过密封case class 或其他类似方式隐藏主构造函数,但感觉有点在这种情况下矫枉过正。

【讨论】:

    【解决方案2】:

    虽然我大多同意@flavian 的回答,即您应该为您的方法使用另一个名称,但您可以做的就是根本不创建案例类。或者更确切地说,手动实现案例类构造为您提供的所有内容:

    class NonNegativeInt private (val i: Int) {
      override def equals(that: Any): Boolean = that.isInstanceOf[NonNegativeInt] && that.asInstanceOf[NonNegativeInt].i == i
      override def hashCode = i.hashCode
    
      def copy(i: Int = this.i) = NonNegativeInt(i)  //use companion apply method, not private constructor
    }
    
    object NonNegativeInt {
      def apply(i: Int) = 
        new NonNegativeInt(if (i < 0) 0 else i)
    
      def unapply(that: NonNegativeInt): Option[Int] = Some(that.i)
    }
    

    【讨论】:

      【解决方案3】:

      据我所知,没有直接的方法可以覆盖案例类构造函数。但是,假设实际数据类型不是简单的 int,您可以执行一些考虑无效状态的类型,如下所示:

      sealed abstract class NonNegativeInt { def isValid: Boolean }
      final case class ValidNonNegativeInt(i: Int) extends NonNegativeInt { override def isValid: Boolean = true }
      final case object InvalidNonNegativeInt extends NonNegativeInt { override def isValid: Boolean = false }
      object NonNegativeInt {
        def apply(i: Int): NonNegativeInt = if (i < 0) InvalidNonNegativeInt else ValidNonNegativeInt(i)
      }
      

      这很简单:

      scala>   NonNegativeInt(0)
      res5: NonNegativeInt = ValidNonNegativeInt(0)
      
      scala>   NonNegativeInt(-1)
      res6: NonNegativeInt = InvalidNonNegativeInt
      

      然后你甚至可以进行模式匹配:

      val ni = NonNegativeInt(10)
      ni match {
          case ValidNonNegativeInt(i) => println(s"valid $i")
          case InvalidNonNegativeInt => println(s"invalid")
      }
      

      然后您可以使用 map/flatMap 等进一步扩展您的功能。

      当然它仍然不能保护你免受负面情况的影响:

      scala>   ValidNonNegativeInt(-10)
      res7: ValidNonNegativeInt = ValidNonNegativeInt(-10)
      

      但例如 scala 选项也不会覆盖 Some() 情况下允许无效值的构造函数:

      scala> Option(null)
      res8: Option[Null] = None
      
      scala> Some(null)
      res9: Some[Null] = Some(null)
      

      除非没有关键用例,否则对于简单的 Int,我会保持原样,并确保其在使用中的正确性。对于更复杂的结构,上述方法非常有用。

      注意:我故意不使用你的 Max(0, n) 方式,因为在这种情况下 会导致比它解决的问题更多的问题。假设某事,并且 在后台交换数据是不好的做法。想象一下你会有一个 在您的代码其他地方的某个地方出现错误,这将使用您的 使用 Max(0, n) 实现。如果输入数据是 -10,很可能, 问题是由传入数据中的其他问题引起的。当你 将其更改为默认值 0,即使输入为 -10,稍后当您 将分析日志、转储或调试输出,你会错过这样一个事实 是-10。


      我认为其他解决方案:

      @flavian 解决方案是最合乎逻辑的。显式功能/验证

      @Cyrille Corpet:非常 Java 风格

      @jwvh 解决方案将占用双倍的内存占用,因为它将在内存中占用两个 Ints。并且也不会防止覆盖:

      scala>   case class NonNegativeInt1(private val x:Int)(implicit val i:Int = Math.max(0,x)) {
           |     override def toString: String = s"NonNegativeInt1($x, $i)"
           |   }
      defined class NonNegativeInt1
      
      scala>   NonNegativeInt1(5)
      res10: NonNegativeInt1 = NonNegativeInt1(5, 5)
      
      scala>   NonNegativeInt1(-5)
      res11: NonNegativeInt1 = NonNegativeInt1(-5, 0)
      
      scala>   NonNegativeInt1(-5)(-5)
      res12: NonNegativeInt1 = NonNegativeInt1(-5, -5)
      

      【讨论】:

        猜你喜欢
        • 2011-02-09
        • 2015-02-01
        • 1970-01-01
        • 2011-01-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-06
        • 2014-01-27
        相关资源
        最近更新 更多