通用包装器如何使Scala中的NaN相等?

问题描述

在阅读了“ Scala编程”一书的第30章有关对象相等性的示例之后,我对如何在通用容器中确保equals方法的反射性感到有些困惑,考虑到NaN比较是不自反的。请考虑以下代码段:

class Wrapper[T](val elem: T) {
  override def equals(other: Any): Boolean = other match {
    case that: Wrapper[_] => this.elem == that.elem
    case _ => false
  }
}

object NaNCompare extends App {
  val nan: Double = 0.0 / 0.0
  val nanw: Wrapper[Double] = new Wrapper(nan)
  val pzero: Double = +0.0
  val pzerow: Wrapper[Double] = new Wrapper(pzero)
  val mzero: Double = -0.0
  val mzerow: Wrapper[Double] = new Wrapper(mzero)
  println(s"nan equals nan: ${nan equals nan}")
  println(s"nan == nan: ${nan == nan}")
  println(s"+0 equals -0: ${pzero equals mzero}")
  println(s"+0 == -0: ${pzero == mzero}")
  println(s"[nan] equals [nan]: ${nanw equals nanw}")
  println(s"[nan] == [nan]: ${nanw == nanw}")
  println(s"[+0] equals [-0]: ${pzerow equals mzerow}")
  println(s"[+0] == [-0]: ${pzerow == mzerow}")
}

这将打印以下内容

nan equals nan: true
nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: true
[nan] == [nan]: true
[+0] equals [-0]: true
[+0] == [-0]: true

前四行是逻辑上的:根据IEEE 754,nan == nan为假,而+0 == -0为真;但是,两个nan是同一个对象,因此nan equals nan必须满足equals的反射性要求; +0 equals -0为假,因为这两个浮点数具有不同的表示形式。到目前为止一切顺利。

但是,Wrapper被通用==包裹时,在比较NaN时突然开始生成true。我首先以为这是由于类型擦除导致的,所以它基本上比较位表示(对于相同的NaN来说是相等的),但是如果是这样,则它必须在比较加零和减零的包装时打印false 。但是,这两种情况都是正确的。

为了使您更加困惑,如果在代码的第一行(Double)的上一行添加class Wrapper[T <: Double](val elem: T) {,它将显示以下内容

nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: false
[nan] == [nan]: false
[+0] equals [-0]: true
[+0] == [-0]: true

因此,如果它受Double的限制,则包裹的NaN不再相等!如果用AnyVal绑定,则它们与原始代码中的相等。如果使包装器为非通用包装器(删除T参数并用Double代替),则它们是不相等的。因此很明显,在不同情况下,编译器“记住” Double的内部结构。但是,它到底能记住什么以及如何执行==的分派?

解决方法

我怀疑正在发生的事情是Scala可以使用两种不同的IEEE双精度运行时实现,并且它们的相等语义略有不同。

  • 对于Java原语double,比较是使用特定的JVM指令实现的; +0.0 == -0.0true,而Double.NaN == Double.NaN为假
  • 对于java.lang.DoubleObject的{​​{1}}框),double是通过比较位表示来实现的,这导致equals比较{{1 }}和NaN不等于true

在Scala中,只要证明它正在处理+0(并且没有-0的方法都可以使用),编译器就基本上会使用原语double(因为它快得多)。被称为,例如scala.Double,并且在不确定时将使用java.lang.Object(例如,在通用名称(尚未使用equals ...)中。 Scala java.lang.Double方法并不是@specialized的同义词,因为它对在JVM中作为原语实现的事物有特殊的处理。

通常,应将Scala代码中==的用法替换为.equals