第6章 函数式对象
有了从前几章获得的Scala基础知识,你已经为探索如何在Scala里设计出更全面特征的对象做好了准备。本章的重点在于定义函数式对象,也就是说,没有任何可变状态的对象的类。作为运行的例子,我们将创造若干把分数作为不可变对象建模的类的变体。在这过程中,我们会展示给你Scala面向对象编程的更多方面:类参数和构造函数,方法和操作符,私有成员,子类方法重载,先决条件检查,同类方法重载和自指向。
6.1 类Rational的式样书
分数:rational number是一种可以表达为比率dn的数字,这里的n和d是数字,其中d不能为零。n被称作是分子:numerator,d被称作是分母:denominator。分数的例子有:1/2,2/3,112/239和2/1。与浮点数相比较,分数的优势是小数部分得到了完全表达,没有舍入或估算。 本章我们将要设计的类必须模型化分数的行为,包括允许它们执行加,减,乘还有除运算。要加两个分数,首先要获得公分母,然后才能把两个分子相加。例如,要计算1/2+2/3.先把左操作数的上下部分都乘上3,右操作数的两部分都乘上2,得到了3/6+4/6。把两个分子相加产生结果,67。要乘两个分数,可以简单的两个分子相乘,然后两个分母相乘。因此,5221×得到了102,还可以简化表示成它的“通常”形式51。除法是把右操作数分子分母调换,然后做乘法。例如1/2 / 3/5与1/2 * 3/5相同,结果是5/6.
一个,或许不怎么重要的,发现是数学上,分数不具有可变的状态。一个分数加到另外一个分数上,产生的结果是一个新的分数。而原来的数不会被“改变”。我们将在本章设计的不可变的Rational类将秉承这一属性。每个分数将都被表示成一个Rational对象。当两个Rational对象相加时,一个新的带着累加结果的Rational对象将被创建出来。 本章还将捎带提一些Scala让你写出感觉像原生语言支持的库的方法。例如,在本章结尾你将能用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 / 7) + (1 twoThirds) res0: Rational = 17/42
6.2 创建Rational
开始设计Rational类的着手点是考虑客户程序员将如何创建一个新的Rational对象。假设我们已决定让Rational对象是不可变的,我们将需要那个客户在创建实例时提供所有需要的数据(本例中,是分子和分母)。因此,我们应该这么开始设计: class Rational(n: Int, d: Int) 这行代码里首先应当注意到的是如果类没有主体,就不需要指定一对空的大括号(当然你如果想的话也可以)。在类名,Rational,之后括号里的n和d,被称为类参数:class parameter。Scala编译器会收集这两个类参数并创造一个带同样的两个参数的主构造器:primary constructor。
//不可变对象的权衡
不可变对象提供了若干强于可变对象的优点和一个潜在的缺点。首先,不可变对象常常比可变对象更具逻辑性,因为它们没有随着时间而变化的复杂的状态空间。其次,你可以很自由地传递不可变对象,而或许需要在把可变对象传递给其它代码之前,需要先建造个以防万一的副本。第三,没有机会能让两个同时访问不可变对象的线程破坏它合理构造的状态,因为根本没有线程可以改变不可变对象的状态。第四,不可变对象让哈希表键值更安全。比方说,如果可变对象在被放进了HashSet之后被改变,那么你下一次查找这个HashSet就找不到这个对象了。 不可变对象唯一的缺点就是它们有时需要复制很大的对象图而可变对象的更新可以在原地发生。有些情况下这会变得难以快速完成而可能产生性能瓶颈。结果,要求库提供可变替代以使其更容易在大数据结构的中间改变一些元素也并非是一件稀奇的事情。例如,类StringBuilder是不可变的String的可变替代。第十八章中我们会给出更多Scala里设计可变对象的细节。//
//注意 这个最初的Rational例子凸显了Java和Scala之间的不同。Java类具有可以带参数的构造器,而Scala类可以直接带参数。Scala的写法更简洁——类参数可以直接在类的主体中使用;没必要定义字段然后写赋值函数把构造器的参数复制到字段里。这可以潜在地节省很多固定写法,尤其是对小类来说。//
Scala编译器将把你放在类内部的任何不是字段的部分或者方法定义的代码,编译进主构造器。例如,你可以像这样打印输出一条除错消息: class Rational(n: Int, d: Int) { println("Created "+n+"/"+d) } 根据这个代码,Scala编译器将把println调用放在Rational的主构造器。因此,println调用将在每次创建一个新的Rational实例时打印这条除错信息:
scala> new Rational(1, 2) Created 1/2 res0: Rational = Rational@a0b0f5
6.3 重新实现toString方法
前例中当Rational实例被创建之后,解释器打印输出“Rational@a0b0f5”。解释器是通过调用Rational对象的toString方法获得的这个看上去有些好玩儿的字串。缺省情况下,Rational类继承了定义在java.lang.Object类上的toString实现,只是打印类名,一个@符号和一个十六进制数。toString的结果主要是想通过提供可以用在除错时的语句打印,日志消息,测试错误报告和解释器,除错器输出的信息来尝试对程序员提供帮助。目前toString提供的结果不会特别有用,因为它没有给出任何它被调用的Rational数值的任何线索。更有用的toString实现应该打印出Rational的分子和分母。你可以通过在Rational类里增加toString方法的方式重载:override缺省的实现,如:
class Rational(n: Int, d: Int) { override def toString = n +"/"+ d } 方法定义前的override修饰符标示了之前的方法定义被重载;第10章会更进一步说明。现在分数显示得很漂亮了,所以我们去掉了前一个版本的Rational类里面的println除错语句。你可以在解释器里测试Rational的新行为: scala> val x = new Rational(1, 3) x: Rational = 1/3 scala> val y = new Rational(5, 7) y: Rational = 5/7
6.4 检查先决条件
下一步,我们将把视线转向当前主构造器行为里的一些问题。如本章早些时候提到的,分数的分母不能为零。然而目前主构造器会接受把零传递给d: scala> new Rational(5, 0) res6: Rational = 5/0 面向对象编程的一个优点就是它允许你把数据封装在对象之内以便于你确保数据在整个生命周期中是有效的。像Rational这样的不可变对象,这就意味着你必须确保在对象创建的时候数据是有效的(并且,确保对象的确是不可变的,这样数据就不会在之后变成无效的状态)。由于零做分母对Rational来说是无效状态,因此在把零传递给d的时候,务必不能让Rational被构建出来。
解决这个问题的最好办法是为主构造器定义一个先决条件:precondition说明d必须为非零值。先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。一种方式是使用require方法,如:
class Rational(n: Int, d: Int) { require(d != 0) override def toString = n +"/"+ d } require方法带一个布尔型参数。如果传入的值为真,require将正常返回。反之,require将通过抛出IllegalArgumentException来阻止对象被构造。
6.5 添加字段
现在主构造器可以正确地执行先决条件,我们将把注意力集中到支持加法。想做到这点,我们将在类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对象。2要想访问that的n和d,需要把它们放在字段中。代码6.1展示了如何把这些字段加入类Rational
在代码6.1展示的Rational版本里,我们增加了两个字段,分别是numer和denom,并用类参数n和d初始化它们。我们还改变了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
6.6 自指向
关键字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 .这里,第一个this是冗余的,你写成(lessThan(that))也是一样的。但第二个this表示了当测试为假的时候的方法的结果;如果你省略它,就什么都返回不了了。
6.7 从构造器
有些时候一个类里需要多个构造器。Scala里主构造器之外的构造器被称为从构造器:auxiliary constructor。比方说,分母为1的分数只写分子的话就更为简洁。如,对于15来说,可以只是写成5。因此,如果不是写成Rational(5, 1),客户程序员简单地写成Rational(5)或许会更好看一些。这就需要给Rational添加一个只带一个参数,分子,的从构造器并预先设定分母为1。代码6.2展示了应该有的样子。 class Rational(n: Int, d: Int) { require(d != 0) val numer: Int = n val denom: Int = d def this(n: Int) = this(n, 1) override def toString = numer+"/"+denom def add(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) }
代码6.2 带有从构造器的Rational Scala的从构造器开始于def this(...)。Rational的从构造器主体几乎完全是调用主构造器,直接传递了它的唯一的参数,n,作为分子和1作为分母。输入下列代码到解释器里可以实际看到从构造器的效果: scala> val y = new Rational(3) y: Rational = 3/1 Scala里的每一个从构造器的第一个动作都是调用同一个类里面其他的构造器。换句话说就是,每个Scala类里的每个从构造器都是以“this(...)”形式开头的。被调用的构造器既可以是主构造器(好像Rational这个例子),也可以是从文本上来看早于调用构造器的其它从构造器。这个规则的根本结果就是每一个Scala的构造器调用终将结束于对类的主构造器的调用。因此主构造器是类的唯一入口点。 注意 若你熟悉Java,你或许会奇怪为什么Scala构造器的规矩比Java的还要大。Java里,构造器的第一个动作必须要么调用同类里的另一个构造器,要么直接调用超类的构造器。Scala的类里面,只有主构造器可以调用超类的构造器。Scala里更严格的限制实际上是权衡了更高的简洁度和与Java构造器相比的简易性所付出的代价之后作出的设计。超类,构造器调用和继承交互的细节将在第10章里解释。
6.8 私有字段和方法
上一个版本的Rational里,我们只是分别用n初始化了numer,用d初始化了denom。结果,Rational的分子和分母可能比它所需要的要大。例如分数4266,可以更约简化为相同的最简形式,711,但Rational的主构造器当前并不做这个工作: scala> new Rational(66, 42) res15: Rational = 66/42 要想对分数进行约简化,需要把分子和分母都除以最大公约数:greatest common divisor。如:66和42的最大公约数是6。(另一种说法就是,6是能够除尽66和42的最大的整数。)4266的分子和分母都除以6就产生它的最简形式,711。代码6.3展示了如何做到这点: 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 def this(n: Int) = this(n, 1) def add(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) override def toString = numer+"/"+denom private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) }
代码6.3 带私有字段和方法的Rational 这个版本的Rational里,我们添加了私有字段,g,并修改了numer和denom的初始化器(初始化器:initializer是初始化变量,例如初始化numer的“n / g”,的代码)。因为g是私有的,它只能在类的主体之内,而不能在外部被访问。我们还添加了一个私有方法,gcd,用来计算传入的两个Int的最大公约数。比方说,gcd(12, 8)是4。正如你在4.1节中看到的,想让一个字段或方法私有化你只要把private关键字放在定义的前面。私有的“助手方法”gcd的目的是把类的其它部分,这里是主构造器,需要的代码分离出来。为了确保g始终是正的,我们传入n和d的绝对值,调用abs即可获得任意整数的绝对值。
Scala编译器将把Rational的三个字段的初始化代码依照它们在源代码中出现的次序放入主构造器。所以g的初始化代码,gcd(n.abs, d.abs),将在另外两个之前执行,因为它在源文件中出现得最早。g将被初始化为类参数,n和d,的绝对值的最大公约数。然后再被用于numer和denom的初始化。通过把n和d整除它们的最大公约数,g,每个Rational都将被构造成它的最简形式:
scala> new Rational(66, 42) res24: Rational = 11/7
6.9 定义操作符
Rational加法的当前实现仅就完成功能来讲是没问题的,但它可以做得更好用。你或许会问你自己为什么对于整数或浮点数你可以写成: x + y 但是如果是分数就必须写成: x.add(y) 或至少是: x add y 没有合理的解释为什么就必须是这样的。分数和别的数应该是一样的。数学的角度上看他们甚至比,唔,浮点数,更自然。为什么就不能使用自然的数学操作符呢?Scala里面你做得到。本章后续部分,我们会告诉你怎么做。 第一步是用通常的数学的符号替换add方法。这可以直接做到,因为Scala里+是合法的标识符。我们可以用+定义方法名。既然已经到这儿了,你可以同样实现一个*方法以实现乘法,结果展示在代码6.4中: 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 def this(n: Int) = this(n, 1) def +(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) def *(that: Rational): Rational = new Rational(numer * that.numer, denom * that.denom) override def toString = numer+"/"+denom private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) }
代码6.4 带操作符方法的Rational 有了这种方式定义的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 res32: Rational = 7/6 与以往一样,在最后输入的那行里的语法格式相等于一个方法调用。你也能这么写: scala> x.+(y) res33: Rational = 7/6 不过这样写可读性不佳。 另外一件要提的是基于5.8节中提到的Scala的操作符优先级规则,Rational里面的*方法要比+方法绑定得更结实。或者说,Rational涉及到+和*操作的表达式会按照预期的方式那样表现。例如,x + x * y会当作x + (x * y)而不是(x + x) * y: scala> x + x * y res34: Rational = 5/6 scala> (x + x) * y res35: Rational = 2/3 scala> x + (x * y) res36: Rational = 5/6
6.10 Scala的标识符
现在你已经看到了Scala里两种构成标识符的方式:字母数字式和操作符。Scala在构成标识符方面有非常灵活的规则。除了这两种之外你会看到还有其它的两种。本节将说明所有的这四种标识符构成方式。 字母数字标识符:alphanumeric identifier起始于一个字母或下划线,之后可以跟字母,数字,或下划线。‘$’字符也被当作是字母,但是被保留作为Scala编译器产生的标识符之用。用户程序里的标识符不应该包含‘$’字符,尽管能够编译通过;但是这样做有可能导致与Scala编译器产生的标识符发生名称冲撞。
Scala遵循Java的驼峰式5标识符习俗,例如toString和HashSet。尽管下划线在标识符内是合法的,但在Scala程序里并不常用,部分原因是为了保持与Java一致,同样也由于下划线在Scala代码里有许多其它非标识符用法。因此,最好避免使用像to_string,__init__,或name_这样的标识符。字段,方法参数,本地变量,还有函数的驼峰式名称,应该以小写字母开始,如:length,flatMap,还有s。类和特质的驼峰式名称应该以大写字母开始,如:BigInt,List,还有UnbalancedTreeMap。
//注意 标识符结尾使用下划线的一个结果就是,比如说,如果你尝试写一个这样的定义,“val name_: Int = 1”,你会收到一个编译器错误。编译器会认为你正常是定义一个叫做“name_:”的变量。要让它编译通过,你将需要在冒号之前插入一个额外的空格,如:“val name_ : Int = 1”。//
Scala与Java的习惯不一致的地方在于常量名。Scala里,constant这个词并不等同于val。尽管val在被初始化之后的确保持不变,但它还是个变量。比方说,方法参数是val,但是每次方法被调用的时候这些val都可以代表不同的值。而常量更持久。比方说,scala.Math.Pi被定义为很接近实数π的双精度值,表示圆周和它的直径的比值。这个值不太可能改变,因此Pi显然是个常量。你还可以用常数去给一些你代码里作为幻数:magic number要用到的值一个名字:文本值不具备解释能力,如果出现在多个地方将会变得极度糟糕。你还可能会需要定义用在模式匹配里的常量,用例将在15.2节中说明。Java里,习惯上常量名全都是大写的,用下划线分隔单词,如MAX_VALUE或PI。Scala里,习惯只是第一个字母必须大写。因此,Java风格的常量名,如X_OFFSET,在Scala里也可以用,但是Scala的惯例是常数也用驼峰式风格,如XOffset。
操作符标识符:operator identifier由一个或多个操作符字符组成。操作符字符是一些如+,:,?,~或#的可打印的ASCII字符。以下是一些操作符标识符的例子: + ++ ::: <?> :-> Scala编译器将内部“粉碎”操作符标识符以转换成合法的内嵌‘$’的Java标识符。例如,标识符:->将被内部表达为$colon$minus$greater。若你想从Java代码访问这个标识符,就应使用这个内部表达。 Scala里的操作符标识符可以变得任意长,因此在Java和Scala间有一些小差别。Java里,输入x<-y将会被拆分成四个词汇符号,所以写成x < - y也没什么不同。Scala里,<-将被作为一个标识符拆分,而得到x <- y。如果你想要得到第一种解释,你要在‘<’和‘-’字符间加一个空格。这大概不会是实际应用中的问题,因为没什么人会在Java里写x<-y的时候不注意加空格或括号的。 混合标识符:mixed identifier由字母数字组成,后面跟着下划线和一个操作符标识符。例如,unary_+被用做定义一元的‘+’操作符的方法名。或者,myvar_=被用做定义赋值操作符的方法名。多说一句,混合标识符格式myvar_=是由Scala编译器产生的用来支持属性:property的;第十八章进一步说明。 文本标识符:literal identifier是用反引号`...`包括的任意字串。如: `x` `<clinit>` `yield` 它的思路是你可以把任何运行时认可的字串放在反引号之间当作标识符。结果总是Scala标识符。即使包含在反引号间的名称是Scala保留字,这个规则也是有效的。在Java的Thread类中访问静态的yield方法是其典型的用例。你不能写Thread.yield()因为yield是Scala的保留字。然而,你仍可以在反引号里引用方法的名称,例如Thread.`yield`()。
看上去不漂亮。为了让Rational用起来更方便,可以在类上增加能够执行分数和整数之间的加法和乘法的新方法。既然已经到这里了,还可以再加上减法和除法。结果展示在代码6.5中: 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 def this(n: Int) = this(n, 1) def +(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) def +(i: Int): Rational = new Rational(numer + i * denom, denom) def -(that: Rational): Rational = new Rational( numer * that.denom - that.numer * denom, denom * that.denom ) def -(i: Int): Rational = new Rational(numer - i* denom, denom) def *(that: Rational): Rational = new Rational(numer * that.numer, denom * that.denom) def *(i: Int): Rational = new Rational(numer * i, denom) def /(that: Rational): Rational = new Rational(numer * that.denom, denom * that.numer) def /(i: Int): Rational = new Rational(numer, denom * i) override def toString = numer+"/"+denom private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) }
代码6.5 含有重载方法的Rational 现在每种数学方法都有两个版本了:一个带分数做参数,另一个带整数。或者可以说,这些方法名都被重载:overload了,因为每个名字现在都被多个方法使用。例如,+这个名字被一个带Rational的和另一个带Int的方法使用。方法调用里,编译器会拣出正确地匹配了参数类型的重载方法版本。例如,如果x.+(y)的参数y是Rational,编译器就会拣带有Rational参数的+方法来用。相反如果参数是整数,编译器就会拣带有Int参数的+方法做替代。如果你尝试输入: scala> val x = new Rational(2, 3) x: Rational = 2/3 scala> x * x res37: Rational = 4/9 scala> x * 2 res38: Rational = 4/3 你会看到*方法的调用取决于每个例子里面右侧操作数的类型。
// 注意 Scala分辨重载方法的过程与Java极为相似。任何情况下,被选中的重载版本都是最符合参数静态类型的那个。有时如果不止一个最符合的版本;这种情况下编译器会给你一个“参考模糊”的错误。
6.12 隐式转换
现在你能写r * 2了,或许你想交换操作数,就像2 * r这样。不幸的是这样做还不可以: scala> 2 * r <console>:7: error: overloaded method value * with alternatives (Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int <and> (Char)Int <and> (Short)Int <and> (Byte)Int cannot be applied to (Rational) val res2 = 2 * r ˆ 这里的问题是2 * r等同于2.*(r),因此这是在整数2上的方法调用。但Int类没有带Rational参数的乘法——没办法,因为类Rational不是Scala库的标准类。 然而,Scala里有另外一种方法解决这个问题:你可以创建一个在需要的时候能自动把整数转换为分数的隐式转换。试着把这行代码加入到解释器: scala> implicit def intToRational(x: Int) = new Rational(x) 这行代码定义了从Int到Rational的转换方法。方法前面的implicit修饰符告诉编译器若干情况下自动调用它。定义了转换之后,你现在可以重试之前失败的例子了: scala> val r = new Rational(2,3) r: Rational = 2/3 scala> 2 * r res0: Rational = 4/3 请注意隐式转换要起作用,需要定义在作用范围之内。如果你把隐式方法定义放在类Rational之内,它就不在解释器的作用范围。现在,你要在解释器内直接定义它。 正如你在这个例子中能领略到的,隐式转换是把库变得更灵活和更方便的非常强大的技术。因为他们如此强大,所以也很容易被误用。第二十一章里你将发现隐式转换的更多细节,包括在需要的时候把它们带入作用范围的方式。
6.13 一句警告
如本章所演示的,用操作符名称来创建方法并定义隐式转换能帮助你设计出让客户代码更简洁和易于理解的库。Scala给了你大量的设计这种易于使用库的能力,不过请牢记能力带来的责任。 如果无技巧性地使用,操作符方法和隐式转换都会让客户代码变得难以阅读和理解。因为隐式转换是由编译器隐式地应用的,而不是显式地写在源代码中,对于客户程序员来说哪个隐式转换被应用了并非显而易见。而且尽管操作符方法通常会使得客户代码更简洁,但它只会在客户程序员能够识别和记住每个操作符的意思的程度上让程序变得更易读。 在设计库的时候你应记在脑袋里的目标,并不是仅仅让客户代码简洁,而是让它变得更可读,更易懂。简洁性经常是可读性的重要部分,但不能简洁的过了头。通过设计出有助于简洁,可读,易懂的客户代码的库,你将帮助客户程序员更多产地工作。
6.14 结语
本节中,你看到了Scala类中更多的元素。你看到了如何向类添加参数,如何定义若干构造函数,如何像方法那样定义操作符,以及如何把让类使用起来更自然。或许最重要的是,你在本章中发现定义和使用不可变状态对象在Scala里是一种非常自然的方式。
尽管本章中显示的Rational最终版本满足了开始时候的需求集,它仍有改善的空间。实际上当你学了一定的知识能把Rational变得更好的时候,我们将在后续的书里回到这个例子上。例如,在第二十八章,你会学到如何重载equals和hashcode来允许Rational在用==比较或放入到哈希表时表现得更好。在第二十一章,你会学到如何把隐式方法定义放在Rational的伴生对象中,这样当客户程序员在使用Rational的时候就可以更容易地把它们放在作用范围中。
6.11 方法重载
回到类Rational上来。在最近一次改变之后,你可以在分数上用自然的风格做加法和乘法。但别忘了还有混合运算。例如,你不能把一个分数和一个整数乘在一起,因为‘*’的操作数只能是分数。所以对于分数r你不能写r * 2。而必须写成r * new Rational(2),