【问题标题】:How to define custom equality in case classes如何在案例类中定义自定义相等
【发布时间】:2015-08-10 08:36:55
【问题描述】:

我在下面定义了一个案例类Foo。我想覆盖== 的行为,以便在比较中忽略最后一个元素(optBar)。这是我尝试过的方法,它似乎有效。

case class Bar(i:Int)
case class Foo(i:Int, s:String, optBar:Option[Bar]) {
    override def equals(o:Any) = o match {
        case Foo(`i`, `s`, _) => true
        case _ => false
    }
    override def hashCode = i.hashCode*997  ^ s.hashCode * 991
}
val b = Bar(1)
val f1 = Foo(1, "hi", Some(b))
val f2 = Foo(1, "hi", None)
f1 == f2 // true

我想知道hashCode的创建方法是否正确。我是从this link 那里得到的。

【问题讨论】:

    标签: scala


    【解决方案1】:

    您的 hashCode 定义是正确的,因为它符合 equals/hashCode 合同。不过我觉得

    override def hashCode = (i, s).##
    

    更好读。

    澄清这是做什么的:## 只是一个调用 hashCode 的 convenience method on scala.Any,但可以正确处理 null 和一些与原语相关的极端情况。

    val x: String = null
    x.## // works fine. returns 0
    x.hashCode // throws NullPointerException
    

    So (i, s).## 创建一个 i 和 s 的元组(它有一个明确定义的 hashCode 方法),然后返回它的哈希码。因此,您不必手动编写涉及 MurmurHash 等的哈希码方法。顺便说一句:如果元组的元素之一为空,这也将正常工作,而像问题中的那样手写哈希方法可能会抛出 NPE。

    但是,根据我的经验,如果您想修改案例类为您提供的任何内容,您并不真正需要案例类。此外,在不考虑某些数据的情况下覆盖相等性在某些时候似乎是一个聪明的想法,但它可能会导致一些非常混乱的行为。

    【讨论】:

    • 我同意“导致混乱行为”的部分,所以我正在重新考虑是否真的可以不用它。我可以,但我需要添加额外的逻辑来处理Foo 中的最后一个字段。我需要权衡选择。
    • @Jus12 你可以写case class Foo(i:Int, s:String)(optBar:Option[Bar])。第二个参数列表不被认为是生成的案例类方法的一部分。
    • 被接受为正确答案,因为override def hashCode = (i, s).## 更好。你能详细说明它的作用吗? :)
    • 我在答案中添加了一些关于##的解释
    【解决方案2】:

    您还可以从案例类定义中删除optBar,并使用三个参数创建一个构造函数。为避免在您想使用该构造函数时必须使用 new 关键字,您可以创建一个伴随对象。

    case class Bar(i:Int)
    case class Foo(i:Int, s:String) {
      var optBar: Option[Bar] = None
    
      def this(i:Int, s:String, optBar:Option[Bar]) {
        this(i, s)
        this.optBar = optBar
      }
    }
    object Foo {
      def apply(i:Int, s:String, optBar:Option[Bar]) =
        new Foo(i, s, optBar)
    }
    
    val b = Bar(1)
    val f1 = Foo(1, "hi", Some(b))
    val f2 = Foo(1, "hi", None)
    f1 == f2 // true
    

    【讨论】:

    • 我也喜欢这个,但似乎有点 hackish。 +1 来自我。 :)
    【解决方案3】:

    如何为您自己的相等版本使用不同的运算符。我认为这比覆盖 == 的默认行为更好,例如~= 为“大约相等”

    case class Bar(i:Int)
    case class Foo(i:Int, s:String, optBar:Option[Bar]) {
    
      def ~= (that:Foo): Boolean = (this.i, this.s) == (that.i, that.s)
    
    }
    
    val foo1 = Foo(1, "a", None)
    
    val foo2 = Foo(1, "a", Some(Bar(4)))
    
    foo1 == foo2 //false
    
    foo1 ~= foo2 //true
    

    编辑:

    如果您希望能够将其用作 Map 键,那么我会尝试:

    case class Bar(i: Int)
    
    trait FooLike {
      def s: String
      def i: Int
    
      def ~=(that: FooLike) = (s, i) == (that.s, that.i) 
    }
    
    case class SubFoo(s: String, i: Int) extends FooLike
    
    case class Foo(sub: SubFoo, barOpt: Option[Bar]) extends FooLike {
      def s = sub.s
      def i = sub.i
    }
    
    val map = scala.collection.mutable.Map.empty[SubFoo, String]
    
    val sub = SubFoo("a", 1)
    
    val foo = Foo(sub, None)
    
    foo ~= sub //true
    
    val foo2 = Foo(sub, Some(Bar(1)))
    
    foo ~= foo2 ///true
    
    map += sub -> "abc"
    
    map.get(foo.sub) //Some("abc")
    

    【讨论】:

    • 我需要在收藏中使用它。例如,listOfFoos.contains(foo1)。有什么方法可以让我的用例工作吗?
    • 您可以使用listOfFoos.exists(_ =~ foo1),但我意识到contains 只是一个示例,可能还有其他方法在后台依赖==。将is 变成另一个case 类,然后它是Foo 的成员,这样你就可以执行listOfFoos.map(_.subFoo).contains(foo1.subFoo) 之类的操作?
    • 我用错了用例。正如您所说的那样,我们可以contains 等中使用它,但我们可以在地图中使用它吗,例如mapWithKeyFee.get(foo1)
    • 我认为在这种情况下,使用两个级别的案例类是有意义的,所以你可以使用mapWithKeySubFoo.get(foo1.subFoo)。当它的一个参数不被视为这样的平等的一部分时,我看不出使用 Foo 作为映射键的意义。
    • 如果您的地图是Map[SubFoo, Whatever] 类型并且您尝试执行map.get(foo) 您应该会收到编译时错误,因此应该很容易捕获此类错误。
    猜你喜欢
    • 2015-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多