重载运算符的编码风格

Coding style for overloading operators

假设我有一个名为 Rational 的 class,它表示有理数 "purely",即它将 a/b 的表示形式保持为 (a, b) 并实现通常的运算符 +, -, *, / 和其他运算符来处理这些元组,而不是评估每个运算的实际分数。

现在假设我想定义如果我将 Rational 实例添加到 Int 会发生什么,除了已经定义的 Rational 添加到 [=12] 的行为=].然后,当然,我最终可能想将 Rational 添加到 Double,或者添加到 FloatBigInt 其他数字类型...

方法 #1:提供 +(Rational, _) 的几种实现:

def + (that:Rational):Rational  = {
    require(that != null, "Rational + Rational: Provided null argument.")
    new Rational(this.numer * that.denom + that.numer * this.denom, this.denom * that.denom)
}

def + (that:Int): Rational = this + new Rational(that, 1) // Constructor takes (numer, denom) pair

def + (that:BigInt): Rational = ....
.
.
.

方法 #2:Any 上的模式匹配:

def + (that:Any):Rational  = {
    require(that != null, "+(Rational, Any): Provided null argument.")
    that match {
        case that:Rational => new Rational(this.numer * that.denom + that.numer * this.denom, this.denom * that.denom)
        case that:Int | BigInt => new Rational(this.numer + that * this.denom, this.denom) // a /b + c = (a + cb)/b
        case that:Double => ....
        .
        .
        .
        case _ => throw new UnsupportedOperationException("+(Rational, Any): Unsupported operand.")
     }
}

我从模式匹配方法中看到的一个好处是节省了实际源代码行,但可能会降低可读性。也许更重要的是,当我得到一个我没有定义 + 行为的类型时,我可以控制我的行为。我不确定如何通过第一种方法实现这一点,也许是通过在所有其他方法下面添加 Any 的重载?无论哪种方式,这听起来都很危险。

关于应该选择第一种还是第二种方法的想法?是否有任何我没有发现的安全问题?我是否向 ClassCastException 或其他类型的例外敞开心扉?

强制执行 compile-time 错误的方法是通过类型约束、隐式参数等确保 plus 方法实际上不能采用类型 Any

处理此问题的一种方法是使用 scala Numeric 类型 class。为 Rational 创建实例应该是完全可能的,因为您可以轻松实现所有必需的方法,此时您可以将 plus 定义为

def +[T: Numeric](that: T) : Rational

您现在还可以提取隐式 Numeric 参数的 toInt/toLong/toFloat/toDouble 方法如果您愿意,也可以处理未知 classes 而不是抛出运行时错误 - 即使您不这样做,您至少已经大大减少了可以传递的错误类型。

您还可以定义您自己的类型 class 以及您想要支持的类型的适当实例。然后,您可以将加法逻辑留在 + 方法中,或者将其移至 typeclass 实例中:

trait CanBeAdded[T] {
  def add(t: T, rational: Rational) : Rational
}

object CanBeAdded {
  implicit val int = new CanBeAdded[Int] {
    override def add(t: Int, rational: Rational): Rational = ???
  }

  implicit val long = new CanBeAdded[Long] {
    override def add(t: Long, rational: Rational): Rational = ???
  }

  implicit val rational = new CanBeAdded[Rational] {
    override def add(t: Rational, rational: Rational): Unit = ???
  }
}

case class Rational(a: BigInt, b: BigInt) {
  def +[T: CanBeAdded](that: T) = implicitly[CanBeAdded[T]].add(that, this)
}

我喜欢第二个选项,因为我怀疑允许将 Rational 类型添加到任何数字类型是否有意义。您提到您希望 + 能够接受 Doubles,但是精确表示与 Doubles 中经常出现的舍入误差相结合似乎可能会导致一些非常奇怪和违反直觉的行为,结果没有多大意义。