重载运算符的编码风格
Coding style for overloading operators
假设我有一个名为 Rational
的 class,它表示有理数 "purely",即它将 a/b 的表示形式保持为 (a, b)
并实现通常的运算符 +, -, *, /
和其他运算符来处理这些元组,而不是评估每个运算的实际分数。
现在假设我想定义如果我将 Rational
实例添加到 Int
会发生什么,除了已经定义的 Rational
添加到 [=12] 的行为=].然后,当然,我最终可能想将 Rational
添加到 Double
,或者添加到 Float
、BigInt
其他数字类型...
方法 #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
类型添加到任何数字类型是否有意义。您提到您希望 +
能够接受 Double
s,但是精确表示与 Double
s 中经常出现的舍入误差相结合似乎可能会导致一些非常奇怪和违反直觉的行为,结果没有多大意义。
假设我有一个名为 Rational
的 class,它表示有理数 "purely",即它将 a/b 的表示形式保持为 (a, b)
并实现通常的运算符 +, -, *, /
和其他运算符来处理这些元组,而不是评估每个运算的实际分数。
现在假设我想定义如果我将 Rational
实例添加到 Int
会发生什么,除了已经定义的 Rational
添加到 [=12] 的行为=].然后,当然,我最终可能想将 Rational
添加到 Double
,或者添加到 Float
、BigInt
其他数字类型...
方法 #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
类型添加到任何数字类型是否有意义。您提到您希望 +
能够接受 Double
s,但是精确表示与 Double
s 中经常出现的舍入误差相结合似乎可能会导致一些非常奇怪和违反直觉的行为,结果没有多大意义。