簡體   English   中英

在 Scala 中實現 equals 和 hashCode 的標准習語是什么?

[英]What is the standard idiom for implementing equals and hashCode in Scala?

在 Scala 中實現equalshashCode方法的標准習慣用法是什么?

我知道在 Scala 編程中討論了首選方法,但我目前無法訪問這本書。

有一個免費的第 1 版 PinS 也討論了這個主題。 但是,我認為最好的來源是 Odersky 討論 Java 中的平等的這篇文章 PinS中的討論是,iirc,這篇文章的縮寫版本。

在做了相當多的研究之后,我找不到一個答案,其中包含 Scala 類(不是 case 類,因為它是自動編譯器生成的,不應被覆蓋)的equalshashCode模式的基本和正確實現。 我確實找到了一個2.10(陳舊)的宏,它勇敢地嘗試解決這個問題。

我最終結合了“Effective Java, 2nd Edition”(Joshua Bloch 着)和這篇文章“How to Write an Equality Method in Java”(Martin Odersky、Lex Spoon 和 Bill Venners 着)中推薦的模式,並創建了我現在用來為我的 Scala 類實現equalshashCode的標准默認模式。

equals模式的主要目標是最大限度地減少為獲得有效且明確的truefalse所需執行的實際比較次數。

此外,當覆蓋equals方法時,應始終覆蓋並重新實現hashCode方法(再次參見“Effective Java, 2nd Edition”(由 Joshua Bloch 撰寫) )。 因此,我在下面的代碼中包含了hashCode方法“模式”,其中還包含有關在實際實現中使用##而不是hashCode重要建議

值得一提的是, super.equalssuper.hashCode中的每一個只有在祖先已經覆蓋它super.hashCode必須調用它。 如果不是,則必須不調用super.*作為java.lang.Object的默認實現( equals比較相同的類實例hashCode很可能將對象的內存地址轉換為整數) ,這兩者將為現在覆蓋的方法破壞指定的equalshashCode協定。

class Person(val name: String, val age: Int) extends Equals {
  override def canEqual(that: Any): Boolean =
    that.isInstanceOf[Person]

  //Intentionally avoiding the call to super.equals because no ancestor has overridden equals (see note 7 below)
  override def equals(that: Any): Boolean =
    that match {
      case person: Person =>
        (     (this eq person)                     //optional, but highly recommended sans very specific knowledge about this exact class implementation
          ||  (     person.canEqual(this)          //optional only if this class is marked final
                &&  (hashCode == person.hashCode)  //optional, exceptionally execution efficient if hashCode is cached, at an obvious space inefficiency tradeoff
                &&  (     (name == person.name)
                      &&  (age == person.age)
                    )
              )
        )
      case _ =>
        false
    }

  //Intentionally avoiding the call to super.hashCode because no ancestor has overridden hashCode (see note 7 below)
  override def hashCode(): Int =
    31 * (
      name.##
    ) + age.##
}

該代碼有許多非常重要的細微差別:

  1. 擴展scala.Equals - 確保包含canEqual方法形式化的equals慣用模式正在完全實施。 雖然擴展它在技術上是可選的,但仍然強烈推薦它。
  2. 相同實例短路 - 測試(this eq person)true可確保不再進行(昂貴的)比較,因為它實際上是同一個實例。 此測試需要在模式匹配內進行,因為eq方法可用於AnyRef ,而不是Anythat的類型)。 並且因為AnyRefPerson的祖先,該技術通過對后代Person進行類型驗證來同時進行兩個類型驗證,這意味着對其所有祖先(包括eq檢查所需的AnyRef進行自動類型驗證。 雖然此測試在技術上是可選的,但仍然強烈推薦。
  3. 檢查thatcanEqual -這是很容易得到這種向后這是錯誤的。 this作為參數在that實例上執行canEqual的檢查至關重要。 雖然它對於模式匹配來說似乎是多余的(假設我們得到了這行代碼, that必須是一個Person實例),我們仍然必須進行方法調用,因為我們不能假設thatPerson的相等兼容的后代(所有Person后代將成功模式匹配為Person )。 如果類被標記為final ,則此測試是可選的,可以安全地刪除。 否則,它是必需的。
  4. 檢查hashCode短路 - 雖然不充分也不是必需的,但如果此hashCode測試為false ,則無需執行所有值級別檢查(第 5 項)。 如果此測試為true ,則實際上需要逐個字段檢查。 此測試是可選的,如果未緩存 hashCode 值並且每個字段的相等性檢查的總成本足夠低,則可以排除此測試。
  5. 每個字段的相等性檢查 - 即使提供了hashCode測試並成功,仍必須檢查所有字段級別的值。 這是因為,盡管這極不可能, 但兩個不同的實例仍然有可能生成完全相同的hashCode值,並且在字段級別仍然實際上並不等效 還必須調用父級的equals以確保也測試祖先中定義的任何其他字段。
  6. 模式匹配case _ => - 這實際上是實現了兩種不同的效果。 首先,Scala 模式匹配保證null在此處正確路由,因此null不必出現在我們純 Scala 代碼中的任何地方。 其次,模式匹配保證無論that是,這不是一個實例Person或其后代之一。
  7. 何時調用super.equalssuper.hashCode有點棘手 - 如果祖先已經覆蓋了這兩個(不應該是任何一個),則必須將super.*合並到您自己的覆蓋實現中。 如果祖先沒有覆蓋兩者,那么您覆蓋的實現必須避免調用super.* 上面的Person代碼示例顯示了沒有覆蓋兩者的祖先的情況。 因此,調用每個super.*方法調用將錯誤地一直到默認的java.lang.Object.*實現,這將使equalshashCode的假定組合合同無效。

這是基於super.equals的代碼,僅當至少有一個祖先已經明確覆蓋equals super.equals使用。

override def equals(that: Any): Boolean =
  ...
    case person: Person =>
      ( ...
                //WARNING: including the next line ASSUMES at least one ancestor has already overridden equals; i.e. that this does not end up invoking java.lang.Object.equals
                &&  (     super.equals(person)     //incorporate checking ancestor(s)' fields
                      &&  (name == person.name)
                      &&  (age == person.age)
                )
            ...
      )
    ...

這是基於super.hashCode的代碼,僅當至少有一個祖先已經明確覆蓋了hashCode super.hashCode使用。

override def hashCode(): Int =
  31 * (
    31 * (
      //WARNING: including the next line ASSUMES at least one ancestor has already overridden hashCode; i.e. that this does not end up invoking java.lang.Object.hashCode
      super.hashCode  //incorporate adding ancestor(s)' hashCode (and thereby, their fields)
    ) + name.##
  ) + age.##

最后一個注意事項:在我為此進行的研究中,我無法相信這種模式存在多少錯誤的實現。 很明顯,這仍然是一個難以准確把握細節的領域:

  1. 在 Scala 中編程,第一版- 錯過了上面的 1、2 和 4。
  2. Alvin Alexander 的 Scala Cookbook - 錯過了 1、2 和 4。
  3. Scala 編程代碼示例- 在生成類的hashCode覆蓋和實現時,在類字段上錯誤地使用.hashCode而不是.## 參見 Tree3.scala

是的,覆蓋equalshashCode在 Java 和 Scala 中都是一項艱巨的任務。 我建議根本不要使用equals ,而是使用類型類( Eq / Eql等)。 它更類型安全(比較不相關類型時編譯器錯誤),更容易實現(沒有覆蓋和類檢查)並且更靈活(您可以編寫與數據類分開的類型類實例)。 Dotty 使用了“多元等式”的概念,它提供了在捕獲一些明顯不正確的equals用法和嚴格相等性檢查之間的選擇。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM