有理数类的表示
实现规范:支持有理数的加减乘除,并支持有理数的规范表示
1.定义Rational
首先,考虑用户如何使用这个类,我们已经决定使用“Immutable”方式来使用Rational对象,我们需要用户在定义Rational对象时提供分子和分母。
class Rational(n:Int, d:Int)
可以看到,和Java不同的是,Scala的类定义可以有参数,称为类参数,如上面的n、d。Scala使用类参数,并把类定义和主构造函数合并在一起,在定义类的同时也定义了类的主构造函数。因此Scala的类定义相对要简洁些。
Scala编译器会编译Scala类定义包含的任何不属于类成员和类方法的其它代码,这些代码将作为类的主构造函数。比如,我们定义一条打印消息作为类定义的代码:
scala> class Rational (n:Int, d:Int) {
| println("Created " + n + "/" +d)
| }
defined class Rational
scala> new Rational(1,2)
Created 1/2
res0: Rational = Rational@22f34036
可以看到创建Ratiaonal对象时,自动执行类定义的代码(主构造函数)。
2.重新定义类的toString方法
上面的代码创建Rational(1,2),Scala 编译器打印出Rational@22f34036,这是因为使用了缺省的类的toString()定义(Object对象的),缺省实现是打印出对象的类名称+“@”+16进制数(对象的地址),显示结果不是很直观,因此我们可以重新定义类的toString()方法以显示更有意义的字符。
在Scala中,你也可以使用override来重载基类定义的方法,而且必须使用override关键字表示重新定义基类中的成员。比如:
scala> class Rational (n:Int, d:Int) {
| override def toString = n + "/" +d
| }
defined class Rational
scala> val x= new Rational(1,3)
x: Rational = 1/3
scala> val y=new Rational(5,7)
y: Rational = 5/7
3.前提条件检查
前面说过有理数可以表示为 n/d(其中d、n为正数,而d不能为0)。对于前面的Rational定义,我们如果使用0,也是可以的。
怎么解决分母不能为0的问题呢?面向对象编程的一个优点是实现了数据的封装,你可以确保在其生命周期过程中是有效的。对于有理数的一个前提条件是分母不可以为0,Scala中定义为传入构造函数和方法的参数的限制范围,也就是调用这些函数或方法的调用者需要满足的条件。Scala中解决这个问题的一个方法是使用require方法(require方法为Predef对象的定义的一个方法,Scala环境自动载入这个类的定义,因此无需使用import引入这个对象),因此修改Rational定义如下:
scala> class Rational (n:Int, d:Int) { | require(d!=0) | override def toString = n + "/" +d | }defined class Rational scala> new Rational(5,0) java.lang.IllegalArgumentException: requirement failed at scala.Predef$.require(Predef.scala:211) ... 33 elided
可以看到,如果再使用0作为分母,系统将抛IllegalArgumentException异常。
4.添加成员变量
前面我们定义了Rational的主构造函数,并检查了输入不允许分母为0。下面我们就可以开始实行两个Rational对象相加的操作。我们需要实现的函数化对象,因此Rational的加法操作应该是返回一个新的Rational对象,而不是返回被相加的对象本身。我们很可能写出如下的实现:
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)}
实际上编译器会给出编译错误。
这是为什么呢?尽管类参数在新定义的函数的访问范围之内,但仅限于定义类的方法本身(比如之前定义的toString方法,可以直接访问类参数),但对于that来说,无法使用that.d来访问d。因为that不在定义的类可以访问的范围之内。此时需要定类的成员变量。
注:后面定义的case class类型编译器自动把类参数定义为类的属性,这是可以使用that.d等来访问类参数)。
修改Rational定义,使用成员变量定义如下:
class Rational (n:Int, d:Int) {
require(d!=0)
val number =n
val denom =d
override def toString = number + "/" +denom
def add(that:Rational) = new Rational( number * that.denom + that.number* denom, denom * that.denom )}
要注意的我们这里定义成员变量都使用了val,因为我们实现的是“immutable”类型的类定义。number和denom以及add都可以不定义类型,Scala编译能够根据上下文推算出它们的类型。
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 scala> oneHalf.number res1: Int = 1
5.自身引用
Scala 也使用this来引用当前对象本身,一般来说访问类成员时无需使用this,比如实现一个lessThan方法,下面两个实现是等效的。
第一种:
def lessThan(that:Rational) = this.number * that.denom < that.number * this.denom
第二种:
def lessThan(that:Rational) = number * that.denom < that.number * denom
但如果需要引用对象自身,this就无法省略,比如下面实现一个返回两个Rational中比较大的一个值的一个实现:
def max(that:Rational) = if(lessThan(that)) that else this
其中的this就无法省略。
6.辅助构造函数
在定义类时,很多时候需要定义多个构造函数,在Scala中,除主构造函数之外的构造函数都称为辅助构造函数(或是从构造函数),比如对于Rational类来说,如果定义一个整数,就没有必要指明分母,此时只要整数本身就可以定义这个有理数。我们可以为Rational定义一个辅助构造函数,Scala定义辅助构造函数使用 this(…)的语法,所有辅助构造函数名称为this。
def this(n:Int) = this(n,1)
**所有Scala的辅助构造函数的第一个语句都为调用其它构造函数**,也就是this(…)。被调用的构造函数可以是主构造函数或是其它构造函数(最终会调用主构造函数)。这样使得每个构造函数最终都会调用主构造函数,从而使得主构造函数称为创建类单一入口点。在Scala中也只有主构造函数才能调用基类的构造函数,这种限制有它的优点,使得Scala构造函数更加简洁和提高一致性。
7.私有成员变量和方法
Scala 类定义私有成员的方法也是使用private修饰符,为了实现Rational的规范化显示,我们需要使用一个求分子和分母的最大公倍数的私有方法gcd。同时我们使用一个私有变量g来保存最大公倍数,修改Rational的定义:
scala> class Rational (n:Int, d:Int) { | require(d!=0) | private val g =gcd (n.abs,d.abs) | val number =n/g | val denom =d/g | override def toString = number + "/" +denom | def add(that:Rational) = | new Rational( | number * that.denom + that.number* denom, | denom * that.denom | ) | def this(n:Int) = this(n,1) | private def gcd(a:Int,b:Int):Int = | if(b==0) a else gcd(b, a % b) | } defined class Rational scala> new Rational ( 66,42) res0: Rational = 11/7
注意gcd的定义,因为它是个回溯函数,必须定义返回值类型。**Scala 会根据成员变量出现的顺序依次初始化它们**,因此g必须出现在number和denom之前。
8.定义运算符
本篇还将接着上篇Rational类,我们使用add定义两个Rational对象的加法。两个Rational加法可以写成x.add(y)或者x add y。
即使使用 x add y还是没有 x + y来得简洁。
我们前面说过,在Scala中,运算符(操作符)和普通的方法没有什么区别,任何方法都可以写成操作符的语法。比如上面的 x add y。
而在Scala中对方法的名称也没有什么特别的限制,你可以使用符号作为类方法的名称,比如使用+、-和*等符号。因此我们可以重新定义Rational如下:
class Rational (n:Int, d:Int) { require(d!=0) private val g =gcd (n.abs,d.abs) val numer =n/g val denom =d/g override def toString = numer + "/" +denom def +(that:Rational) = new Rational( numer * that.denom + that.numer* denom, denom * that.denom ) def * (that:Rational) = new Rational( numer * that.numer, denom * that.denom) def this(n:Int) = this(n,1) private def gcd(a:Int,b:Int):Int = if(b==0) a else gcd(b, a % b)}
这样就可以使用 +、*号来实现Rational的加法和乘法。**+、*的优先级是Scala预设的**,和整数的+、-、*和/的优先级一样。下面为使用Rational的例子:
scala> val x= new Rational(1,2)
x: Rational = 1/2 scala> val y=new Rational(2,3) y: Rational = 2/3 scala> x+y res0: Rational = 7/6 scala> x+ x*y res1: Rational = 5/6
从这个例子也可以看出Scala语言的扩展性,你使用Rational对象就像Scala内置的数据类型一样。
9.Scala中的标识符
从前面的例子我们可以看到Scala可以使用两种形式的标志符,字符数字和符号。字符数字使用字母或是下划线开头,后面可以接字母或是数字,符号“$”在Scala中也看作为字母。然而以“$”开头的标识符为保留的Scala编译器产生的标志符使用,应用程序应该避免使用“$”开始的标识符,以免造成冲突。
Scala的命名规则采用和Java类似的camel命名规则(驼峰命名法),首字符小写,比如toString。类名的首字符还是使用大写。此外也应该避免使用以下划线结尾的标志符以避免冲突。
符号标志符包含一个或多个符号,如+、:和?。对于+、++、:::、<、 ?>、 :->之类的符号,Scala内部实现时会使用转义的标志符。例如对:->使用$colon$minus$greater来表示这个符号。因此,如果你需要在Java代码中访问:->方法,你需要使用Scala的内部名称$colon$minus$greater。
混合标志符由字符数字标志符后面跟着一个或多个符号组成,如 unary_+为Scala对+方法的内部实现时的名称。
字面量标志符为使用‘’定义的字符串,比如 ‘x’ 、‘yield’ 。 你可以在‘’之间使用任何有效的Scala标志符,Scala将它们解释为一个Scala标志符,一个典型的使用是 Thread的yield方法, 在Scala中你不能使用Thread.yield()是因为yield为Scala中的关键字, 你必须使用 Thread.‘yield’()来使用这个方法。
10.方法重载
和Java一样,Scala也支持方法重载,重载的方法参数类型不同而使用同样的方法名称,比如对于Rational对象,+的对象可以为另外一个Rational对象,也可以为一个Int对象,此时你可以重载+方法以支持和Int相加。
def + (i:Int) = new Rational (numer + i * denom, denom)
11.隐式类型转换
上面我们定义Rational的加法,并重载+以支持整数,r + 2,当如果我们需要 2 + r如何呢?
可以看到 x + 3没有问题,3 + x就报错了,这是因为整数类型不支持和Rational相加。我们不可能去修改Int的定义(除非你重写Scala的Int定义)以支持Int和Rational相加。如果你写过.Net代码,这可以通过静态扩展方法来实现,Scala提供了类似的机制来解决这种问题。
如果Int类型能够根据需要自动转换为Rational类型,那么 3 + x就可以相加。Scala通过implicit def定义一个隐含类型转换,比如定义由整数到Rational类型的转换如下:
implicit def intToRational(x:Int) = new Rational(x)
其实此时Rational的一个+重载方法是多余的, 当Scala计算 2 + r,发现 2(Int)类型没有可以和Rational对象相加的方法,Scala环境就检查Int的隐含类型转换方法是否有合适的类型转换方法,类型转换后的类型支持+ r,一检查发现定义了由Int到Rational的隐含转换方法,就自动调用该方法,把整数转换为Rational数据类型,然后调用Rational对象的 +方法。从而实现了Rational类或是Int类的扩展。关于implicit def的详细介绍将由后面的文章来说明,隐含类型转换在设计Scala库时非常有用。