Scala 类和对象(二)
Scala 类和对象(二)
本章知识点概括
- 类的定义规范
- 定义类
- 前提条件检查
- 添加成员变量
- 自身引用
- 辅助构造函数
- 私有成员变量和方法
- 定义运算符
- 标识符
- 方法重载
- 隐式类型转换
Rational 类的定义规范
首先,我们回忆下有理数的定义:一个有理数(rational)可以表示成分数形式: n/d , 其中 n 和 d 都是整数( 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 | scala> class Rational (n:Int, d:Int) { |
可以看到创建 Ratiaonal 对象时,自动执行类定义的代码(主构造函数)
重新定义类的 toString 方法
上面的代码创建 Rational(1,2),Scala 编译器打印出 Rational@22f34036,这是因为使用了缺省的类的 toString() 定义 ( Object 对象的), 缺省实现是打印出对象的类名称+ @ + 16进制数(对象的地址),显示结果不是很直观,因此我们可以重新定义类的 toString() 方法以显示更有意义的字符。
在Scala中,你也可以使用 override 来重载基类定义的方法,而且必须使用 override 关键字表示重新定义基类中的成员。比如:
1 | scala> class Rational (n:Int, d:Int) { |
前提条件检查
前面说过有理数可以表示为 n/d (其中 d 、*n*** 、为整数,而 *d*** 不能为 0)。对于前面的 Rational 定义,我们如果使用 0 也是可以的。
1 | scala> new Rational(5,0) |
怎么解决分母不能为0的问题?面向对象编程的一个优点是实现了数据的封装,你可以确保再起声明周期过程中是有效的。对于有理数的一个前提条件是分母不可以为0,Scala中定义为传入构造函数和方法的参数的限制范围,也就是调用这些函数或方法的调用者需要满足的条件。Scala中解决这个问题的一个方法时使用 require 方法 ( require 方法为 Predef 对象定义的一个方法,Scala环境自动载入这个类的定义,因此无需使用 import 引入这个对象),因此修改 Rational 定义如下:
1 | scala> class Rational (n:Int, d:Int) { |
可以看到,如果再使用 0 作为分母,系统将抛出 IllegalArgumentException 异常
添加成员变量
前面我们定义了 Rational 的主构造函数,并检查了输入不允许分母为 0 。 下面我们就可以开始实现两个 Rational 对象相加的操作。我们需要实现的是 函数化对象,因此 Rational 的加法操作应该是返回一个新的 Rational 对象,而不是返回被相加的对象本身。我们很可能写出如下的实现:
1 | class Rational (n:Int, d:Int) { |
实际上编译器会给出如下编译错误:
1 | <console>:11: error: value d is not a member of Rational |
这是为什么呢?尽管类参数在新定义的函数的访问范围之内,但仅限于定义类的方法本身(比如之前定义的 toString 方法,可以直接访问类参数),但对于 that 来说,无法使用 that.d 来访问 d 。因为 that 不在定义的类可以访问的范围之内。此时需要定义为类的成员变量。(注:后面定义的 case class 类型编译器自动把类参数定义为类的属性,这时可以使用 that.d 等来访问类参数)。
修改 Rational 定义,使用成员变量定义如下:
1 | class Rational (n:Int, d:Int) { |
要注意的是,我们这里定义成员变量都使用了 val , 因为我们实现的是“immutable”类型的类定义。 number 和 denom 以及 add 都可以不定义类型,Scala 编译器能够根据上下文推算出它们的类型。
1 | scala> val oneHalf=new Rational(1,2) |
可以看到,这时就可以使用 .number 等来访问类的成员变量。
自身引用
Scala 也适用 this 来引用当前对象本身,一般来说访问类成员时无需使用 this,比如实现一个 lessThan 方法,下面两种实现是等效的。
第一种:
1 | def lessThan(that:Rational) = |
第二种:
1 | def lessThan(that:Rational) = |
但如果需要引入对象自身,*this*** 就无法省略,比如下面实现一个返回两个 *Rational*** 中比较大的一个值:
1 | def max(that:Rational) = |
其中的 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 | class Rational (n:Int, d:Int) { |
1 | scala> new Rational ( 66,42) |
注意gcd的定义,因为他是个 回溯 函数,必须定义返回值类型。
Scala 会根据成员变量出现的顺序依次初始化他们,因此 g 必须出现在 number 和 denom 之前
定义运算符
我们使用 add 定义两个 Rational 对象的加法。两个 Rational 加法可以写成 x.add(y) 或者 x add y。
即使使用 x add y 还是没有 x + y 来得简洁
我们在前面说过,在Scala中,运算符(操作符)和普通的方法没有什么区别,任何方法都可以写成操作符的语法。比如上面的 x add y
而在 Scala 中对方法的名称也没有什么特别的限制,你可以使用符号作为类方法的名称,比如使用 +、*-*** 和 ***等符号。因此我们可以重新定义 *Rational*** 如下:
1 | class Rational (n:Int, d:Int) { |
这样就可以使用 + 、 * 号来实现Rational的加法和乘法。 +、***的优先级是 Scala 预设的,和整数的 +、*-**、** 和 */*** 的优先级一样。 下面为使用 Rational 的例子:
1 | scala> val x= new Rational(1,2) |
从这个例子也可以看出 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标志符,一个典型的使用是 Thread 的 yield 方法,在Scala中你不能使用 Thread.yield() 是因为 yield 为 Scala 中的关键字,你必须使用 Thread.”yield”() 来使用这个方法
方法重载
和 Java 一样, Scala也支持方法重载,重载的方法参数类型不同却使用同样的方法名称, 比如对于 Ratioinal 对象, + 的对象可以为另一个 Rational 对象,也可以为一个 Int 对象,此时你可以重载 + 方法以支持和 Int 相加
1 | def + (i:Int) = |
隐式类型转换
上面我们定义 Rational 的加法,并重载 + 以支持整数, r+2, 但如果我们需要 2+r 如何呢?下面例子
1 | scala> val x =new Rational(2,3) |
可以看到 x+3 没有问题, 3+x就报错了,这是因为整数类型不支持和 Rational 相加。 我们不可能去修改 Int 的定义(除非你重写Scala的 Int 定义) 以支持 Int 和 Rational 相加。 如果你写过 .net 代码,这可以通过静态扩展方法来实现, Scala 提供了类似的机制来解决这种问题。
如果 Int 类型能够根据需要自动转换为 Rational 类型, 那么 3 + x 就可以相加。 Scala 通过 implicit def 定义一个隐含类型转换,比如定义由整数到 Rational 类型的转换如下
1 | implicit def intToRational(x:Int) = new Rational(x) |
再次重新计算 r + 2 和 2 + r 的例子:
1 | scala> val r = new Rational(2,3) |
其实此时 Rational 的一个 + 重载方法时多余的,当Scala计算 2 + r,发现 2(Int) 类型没有可以和 Rational 对象相加的方法, 类型转换后的类型支持 +r , 一检查发现定义了由 Int 到 Rational 的隐含转换方法,就自动调用该方法,把整数转换为 Rational 数据类型, 然后调用 Rational 对象的 + 方法。 从而实现了 Rational 类或是 Int 类的扩展。 关于 implicit def 的详细介绍将由后面的文章来说明,隐含类型转换在设计Scala库时非常有用。