Scala 类和对象(二)

本章知识点概括

  • 类的定义规范
  • 定义类
  • 前提条件检查
  • 添加成员变量
  • 自身引用
  • 辅助构造函数
  • 私有成员变量和方法
  • 定义运算符
  • 标识符
  • 方法重载
  • 隐式类型转换

Rational 类的定义规范

首先,我们回忆下有理数的定义:一个有理数(rational)可以表示成分数形式: n/d , 其中 nd 都是整数( d 不可以为 0 ),*n*** 称为分子(numerator),*d*** 为分母(denominator)。和浮点数相比,有理数可以精确表达一个分数,而不会有误差。

因此我们定义的 Rational 类支持上面的有理数的定义。支持有理数的加减乘除,并支持有理数的规范表示,比如 2/10 ,其规范表示为 1/5 。分子和分母的最小公倍数为 1

有了有理数定义的实现规范,我们可以开始设计类 Rational 。一个好的起点是考虑用户如何使用这个类,我们已经决定使用 “Immutable” 方式来使用 Rational 对象,我们需要用户在定义 Rational 对象时提供分子和分母。因此我们可以开始定义 Rational 类如下:

1
class Rational( n:Int, d:Int)

可以看到,和Java不同的是,Scala的类定义可以有参数,称为 类参数 , 如上面的 n、**d**。Scala使用类参数,并把类定义和主构造函数合并在一起,在定义类的同事也定义了累的主构造函数。因此Scala的类定义相对要简洁一些

Scala 编译器会编译Scala类定义包含的任何不属于类成员和类定义的其他代码,这些代码将作为类的主构造函数。比如,我们定义一条打印消息作为类定义的代码:

1
2
3
4
5
6
7
8
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 对象时,自动执行类定义的代码(主构造函数)

重新定义类的 toString 方法

上面的代码创建 Rational(1,2),Scala 编译器打印出 Rational@22f34036,这是因为使用了缺省的类的 toString() 定义 ( Object 对象的), 缺省实现是打印出对象的类名称+ @ + 16进制数(对象的地址),显示结果不是很直观,因此我们可以重新定义类的 toString() 方法以显示更有意义的字符。

在Scala中,你也可以使用 override 来重载基类定义的方法,而且必须使用 override 关键字表示重新定义基类中的成员。比如:

1
2
3
4
5
6
7
8
9
10
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

前提条件检查

前面说过有理数可以表示为 n/d (其中 d 、*n*** 、为整数,而 *d*** 不能为 0)。对于前面的 Rational 定义,我们如果使用 0 也是可以的。

1
2
scala> new Rational(5,0)
res0: Rational = 5/0

怎么解决分母不能为0的问题?面向对象编程的一个优点是实现了数据的封装,你可以确保再起声明周期过程中是有效的。对于有理数的一个前提条件是分母不可以为0,Scala中定义为传入构造函数和方法的参数的限制范围,也就是调用这些函数或方法的调用者需要满足的条件。Scala中解决这个问题的一个方法时使用 require 方法 ( require 方法为 Predef 对象定义的一个方法,Scala环境自动载入这个类的定义,因此无需使用 import 引入这个对象),因此修改 Rational 定义如下:

1
2
3
4
5
6
7
8
9
10
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 异常

添加成员变量

前面我们定义了 Rational 的主构造函数,并检查了输入不允许分母为 0 。 下面我们就可以开始实现两个 Rational 对象相加的操作。我们需要实现的是 函数化对象,因此 Rational 的加法操作应该是返回一个新的 Rational 对象,而不是返回被相加的对象本身。我们很可能写出如下的实现:

1
2
3
4
5
6
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)
}

实际上编译器会给出如下编译错误:

1
2
3
4
5
<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)

这是为什么呢?尽管类参数在新定义的函数的访问范围之内,但仅限于定义类的方法本身(比如之前定义的 toString 方法,可以直接访问类参数),但对于 that 来说,无法使用 that.d 来访问 d 。因为 that 不在定义的类可以访问的范围之内。此时需要定义为类的成员变量。(注:后面定义的 case class 类型编译器自动把类参数定义为类的属性,这时可以使用 that.d 等来访问类参数)。

修改 Rational 定义,使用成员变量定义如下:

1
2
3
4
5
6
7
8
9
10
11
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”类型的类定义。 numberdenom 以及 add 都可以不定义类型,Scala 编译器能够根据上下文推算出它们的类型。

1
2
3
4
5
6
7
8
9
10
11
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

可以看到,这时就可以使用 .number 等来访问类的成员变量。

自身引用

Scala 也适用 this 来引用当前对象本身,一般来说访问类成员时无需使用 this,比如实现一个 lessThan 方法,下面两种实现是等效的。

第一种:

1
2
def lessThan(that:Rational) = 
this.number * that.denom < that.number * this.denom

第二种:

1
2
def lessThan(that:Rational) = 
number * that.denom < that.number * denom

但如果需要引入对象自身,*this*** 就无法省略,比如下面实现一个返回两个 *Rational*** 中比较大的一个值:

1
2
def max(that:Rational) = 
if(lessThan(that)) that else this

其中的 this 就无法省略

辅助构造函数

在定义类时,很多时候需要定义多个构造函数,在Scala中,除主构造函数之外的构造函数都成为辅助构造函数(或是从构造函数),比如对于 Rational 类来说,如果定义一个整数,就没有必要指明分母,此时只要整数本身就可以定义这个有理数。 我们可以为 Rational 定义一个辅助构造函数, Scala 定义辅助构造函数使用 this(……) 的语法,所有辅助构造函数名称为 this

1
def this(n:Int) = this(n,1)

所有Scala的辅助构造函数的第一个语句都为调用其他构造函数,也就是 this(……)。被调用的构造函数可以是主构造函数或是其他构造函数(最终会调用柱构造函数)。 这样使得每个构造函数最终都会调用主构造函数,从而使得柱构造函数称为创建类单一入口点。在Scala中也只有柱构造函数才能调用基类的构造函数,这种限制有它的有点,使得Scala构造函数更加简洁以及提高一致性

私有成员变量和方法

Scala类定义私有成员的方法也是使用 private 修饰符,为了实现 Rational 的规范化显示,我们需要使用一个求分子和分母的最大公约数的私有方法 gcd. 同时我们使用一个私有变量 g 来保存最大公约数,修改 Rational 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}
1
2
scala> new Rational ( 66,42)
res0: Rational = 11/7

注意gcd的定义,因为他是个 回溯 函数,必须定义返回值类型。
Scala 会根据成员变量出现的顺序依次初始化他们,因此 g 必须出现在 numberdenom 之前

定义运算符

我们使用 add 定义两个 Rational 对象的加法。两个 Rational 加法可以写成 x.add(y) 或者 x add y

即使使用 x add y 还是没有 x + y 来得简洁

我们在前面说过,在Scala中,运算符(操作符)和普通的方法没有什么区别,任何方法都可以写成操作符的语法。比如上面的 x add y

而在 Scala 中对方法的名称也没有什么特别的限制,你可以使用符号作为类方法的名称,比如使用 +、*-*** 和 ***等符号。因此我们可以重新定义 *Rational*** 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的例子:

1
2
3
4
5
6
7
8
9
10
11
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 内置的数据类型一样。

Scala 中的标识符

从前面的例子我们可以看到Scala可以使用两种形式的标志符, 字符数字使用字母或是下划线开头,后面可以接字母或是数字,符号 $ 在Scala 中也看做字母。然而以 $ 开头的标识符为保留的 Scala 编译器产生的标志符使用,应用程序应该避免使用 $ 开始的标识符,以免造成冲突

Scala 的命名规则采用和 Java 类似的 camel 命名规则(驼峰命名法),首字符小写,比如 toString 。类名的首字符还是使用大写。此外也应该避免使用以下划线结尾的标志符以避免冲突

符号标志符包含一个或多个符号,如 + 、*:*** 和 *?*。对于 *+++::::->*之类的符号,Scala内部实现时会使用转义的标志符。例如对 *:->* 使用 *$colon$minus$greater*** 来表示这个符号。因此,如果你需要在Java代码中访问 :-> 方法,你需要使用 Scala 的内部名称 $colon$minus$greater

混合标志符由字符数字标志符后面跟着一个或多个符号组成,比如 unary_+ 为Scala 对 + 方法的内部实现时的名称。

字面量标志符为 使用 定义的字符串,比如 “x”“yield”。 你可以在 之间使用任何有效的Scala标志符,Scala将他们解释为一个Scala标志符,一个典型的使用是 Threadyield 方法,在Scala中你不能使用 Thread.yield() 是因为 yield 为 Scala 中的关键字,你必须使用 Thread.”yield”() 来使用这个方法

方法重载

和 Java 一样, Scala也支持方法重载,重载的方法参数类型不同却使用同样的方法名称, 比如对于 Ratioinal 对象, + 的对象可以为另一个 Rational 对象,也可以为一个 Int 对象,此时你可以重载 + 方法以支持和 Int 相加

1
2
def + (i:Int) = 
new Rational (numer + i * denom, denom)

隐式类型转换

上面我们定义 Rational 的加法,并重载 + 以支持整数, r+2, 但如果我们需要 2+r 如何呢?下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
scala> val x =new Rational(2,3)
x: Rational = 2/3

scala> val y = new Rational(3,7)
y: Rational = 3/7

scala> val z = 4
z: Int = 4

scala> x + z
res0: Rational = 14/3

scala> x + 3
res1: Rational = 11/3

scala> 3 + x
<console>:10: error: overloaded method value + with alternatives:
(x: Double)Double <and>
(x: Float)Float <and>
(x: Long)Long <and>
(x: Int)Int <and>
(x: Char)Int <and>
(x: Short)Int <and>
(x: Byte)Int <and>
(x: String)String
cannot be applied to (Rational)
3 + x
^

可以看到 x+3 没有问题, 3+x就报错了,这是因为整数类型不支持和 Rational 相加。 我们不可能去修改 Int 的定义(除非你重写Scala的 Int 定义) 以支持 IntRational 相加。 如果你写过 .net 代码,这可以通过静态扩展方法来实现, Scala 提供了类似的机制来解决这种问题。

如果 Int 类型能够根据需要自动转换为 Rational 类型, 那么 3 + x 就可以相加。 Scala 通过 implicit def 定义一个隐含类型转换,比如定义由整数到 Rational 类型的转换如下

1
implicit def intToRational(x:Int) = new Rational(x)

再次重新计算 r + 22 + r 的例子:

1
2
3
4
5
6
7
8
scala> val r = new Rational(2,3)
r: Rational = 2/3

scala> r + 2
res0: Rational = 8/3

scala> 2 + r
res1: Rational = 8/3

其实此时 Rational 的一个 + 重载方法时多余的,当Scala计算 2 + r,发现 2(Int) 类型没有可以和 Rational 对象相加的方法, 类型转换后的类型支持 +r , 一检查发现定义了由 IntRational 的隐含转换方法,就自动调用该方法,把整数转换为 Rational 数据类型, 然后调用 Rational 对象的 + 方法。 从而实现了 Rational 类或是 Int 类的扩展。 关于 implicit def 的详细介绍将由后面的文章来说明,隐含类型转换在设计Scala库时非常有用。