在Scala中检查先决条件、添加字段和自指向
学习Scala中Rational类的下一步是,我们将把视线转向当前主构造器行为里的一些问题。如本章早些时候提到的,分数的分母不能为零。然而目前主构造器会接受把零传递给d:
51CTO编辑推荐:Scala编程语言专题
scala> new Rational(5, 0) res6: Rational = 5/0
面向对象编程的一个优点就是它允许你把数据封装在对象之内以便于你确保数据在整个生命周期中是有效的。像Rational这样的不可变对象,这就意味着你必须确保在对象创建的时候数据是有效的(并且,确保对象的确是不可变的,这样数据就不会在之后变成无效的状态)。由于零做分母对Rational来说是无效状态,因此在把零传递给d的时候,务必不能让Rational被构建出来。
解决这个问题的最好办法是为主构造器定义一个先决条件:precondition说明d必须为非零值。先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。一种方式是使用require方法,require方法定义在scala包里的孤立对象Predef上。如:
class Rational(n: Int, d: Int) { require(d != 0) override def toString = n +"/"+ d }
require方法带一个布尔型参数。如果传入的值为真,require将正常返回。反之,require将通过抛出IllegalArgumentException来阻止对象被构造。
添加字段
现在主构造器可以正确地执行先决条件,我们将把注意力集中到支持加法。想做到这点,我们将在类Rational上定义一个公开的add方法,它带另一个Rational做参数。为了保持Rational不可变,add方法必须不能把传入的分数加到自己身上。而是必须创建并返回一个全新的带有累加值的Rational。你或许想你可以这么写add:
class Rational(n: Int, d: Int) { // 编译不过 require(d != 0) override def toString = n +"/"+ d def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d) }
很不幸,上面的代码会让编译器提示说:
< console>:11: error: value d is not a member of Rational new Rational(n * that.d + that.n * d, d * that.d) ˆ < console>:11: error: value d is not a member of Rational new Rational(n * that.d + that.n * d, d * that.d) ˆ
尽管类参数n和d都在你的add代码可引用的范围内,但是在调用add的对象中仅能访问它们的值。因此,当你在add的实现里讲n或d的时候,编译器将很高兴地提供给你这些类参数的值。但绝对不会让你使用that.n或that.d,因为that并不指向add被调用的Rational对象。实际上,在that指的是调用add的对象时, Rational可以加到自己身上。但是因为你可以传递任何Rational对象给add,所以编译器仍然不会让你说that.n。要想访问that的n和d,需要把它们放在字段中。代码6.1展示了如何把这些字段加入类Rational。
在代码6.1展示的Rational版本里,我们增加了两个字段,分别是numer和denom,并用类参数n和d初始化它们。尽管n和d是用在类的函数体内,因为他们只是用在构造器之内,Scala编译器将不会为它们自动构造域。所以就这些代码来说,Scala编译器将产生一个有两个Int域的类,一个是numer,另一个是denom。我们还改变了toString和add的实现,让它们使用字段,而不是类参数。类Rational的这个版本能够编译通过,可以通过分数的加法测试它:
class Rational(n: Int, d: Int) { require(d != 0) val numer: Int = n val denom: Int = d override def toString = numer+"/"+denom def add(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) }
代码 6.1 带字段的Rational
scala> val oneHalf = new Rational(1, 2) oneHalf: Rational = 1/2 scala> val twoThirds = new Rational(2, 3) twoThirds: Rational = 2/3 scala> oneHalf add twoThirds res0: Rational = 7/6
另一件之前不能而现在可以做的事是在对象外面访问分子和分母。只要访问公共的numer和denom字段即可:
scala> val r = new Rational(1, 2) r: Rational = 1 / 2 scala> r.numer res7: Int = 1 scala> r.denom res8: Int = 2
自指向
关键字this指向当前执行方法被调用的对象实例,或者如果使用在构造器里的话,就是正被构建的对象实例。例如,我们考虑添加一个方法,lessThan,来测试给定的分数是否小于传入的参数:
def lessThan(that: Rational) = this.numer * that.denom < that.numer * this.denom
这里,this.numer指向lessThan被调用的那个对象的分子。你也可以去掉this前缀而只是写numer;着两种写法是相同的。
举一个不能缺少this的例子,考虑在Rational类里添加max方法返回指定分数和参数中的较大者:
def max(that: Rational) = if (this.lessThan(that)) that else this