Scala编程--函数式对象
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scala编程--函数式对象相关的知识,希望对你有一定的参考价值。
本章的重点在于定义函数式对象,也就是说,没有任何可变状态的对象的类。作为运行的例子,我们将创造若干把分数作为不可变对象建模的类的变体。在这过程中,我们会展示给你Scala面向对象编程的更多方面:类参数和构造函数,方法和操作符,私有成员,子类方法重载,先决条件检查,同类方法重载和自指向。
6.1 类Rational的式样书
一个,或许不怎么重要的,发现是数学上,分数不具有可变的状态。一个分数加到另外一个分数上,产生的结果是一个新的分数。而原来的数不会被“改变”。我们将在本章设计的不可变的Rational类将秉承这一属性。每个分数将都被表示成一个Rational对象。当两个Rational对象相加时,一个新的带着累加结果的Rational对象将被创建出来。
本章还将捎带提一些Scala让你写出感觉像原生语言支持的库的方法。例如,在本章结尾你将能用Rational类这样做:
1 scala> val oneHalf = new Rational(1, 2) 2 oneHalf: Rational = 1/2 3 scala> val twoThirds = new Rational(2, 3) 4 twoThirds: Rational = 2/3 5 scala> (oneHalf / 7) + (1 twoThirds) 6 res0: Rational = 17/42
6.2 创建Rational
开始设计Rational类的着手点是考虑客户程序员将如何创建一个新的Rational对象。假设我们已决定让Rational对象是不可变的,我们将需要那个客户在创建实例时提供所有需要的数据(本例中,是分子和分母)。因此,我们应该这么开始设计:
1 class Rational(n: Int, d: Int)
这行代码里首先应当注意到的是如果类没有主体,就不需要指定一对空的大括号(当然你如果想的话也可以)。在类名,Rational,之后括号里的n和d,被称为类参数:class parameter。Scala编译器会收集这两个类参数并创造一个带同样的两个参数的主构造器:primary constructor。
注意 这个最初的Rational例子凸显了Java和Scala之间的不同。Java类具有可以带参数的构造器,而Scala类可以直接带参数。Scala的写法更简洁——类参数可以直接在类的主体中使用;没必要定义字段然后写赋值函数把构造器的参数复制到字段里。这可以潜在地节省很多固定写法,尤其是对小类来说。
Scala编译器将把你放在类内部的任何不是字段的部分或者方法定义的代码,编译进主构造器。例如,你可以像这样打印输出一条除错消息:
1 class Rational(n: Int, d: Int) { 2 println("Created "+n+"/"+d) 3 }
根据这个代码,Scala编译器将把println调用放在Rational的主构造器。因此,println调用将在每次创建一个新的Rational实例时打印这条除错信息:
1 scala> new Rational(1, 2) 2 Created 1/2 3 res0: Rational = [email protected]
6.3 重新实现toString方法
前例中当Rational实例被创建之后,解释器打印输出“[email protected]”。解释器是通过调用Rational对象的toString方法获得的这个看上去有些好玩儿的字串。缺省情况下,Rational类继承了定义在java.lang.Object类上的toString实现,只是打印类名,一个@符号和一个十六进制数。toString的结果主要是想通过提供可以用在除错时的语句打印,日志消息,测试错误报告和解释器,除错器输出的信息来尝试对程序员提供帮助。目前toString提供的结果不会特别有用,因为它没有给出任何它被调用的Rational数值的任何线索。更有用的toString实现应该打印出Rational的分子和分母。你可以通过在Rational类里增加toString方法的方式重载:override缺省的实现,如:
1 class Rational(n: Int, d: Int) { 2 override def toString = n +"/"+ d 3 }
方法定义前的override修饰符标示了之前的方法定义被重载;第10章会更进一步说明。现在分数显示得很漂亮了,所以我们去掉了前一个版本的Rational类里面的println除错语句。你可以在解释器里测试Rational的新行为
1 scala> val x = new Rational(1, 3) 2 x: Rational = 1/3 3 scala> val y = new Rational(5, 7) 4 y: Rational = 5/7
6.4 检查先决条件
下一步,我们将把视线转向当前主构造器行为里的一些问题。如本章早些时候提到的,分数的分母不能为零。然而目前主构造器会接受把零传递给d:
1 scala> new Rational(5, 0) 2 res6: Rational = 5/0
面向对象编程的一个优点就是它允许你把数据封装在对象之内以便于你确保数据在整个生命周期中是有效的。像Rational这样的不可变对象,这就意味着你必须确保在对象创建的时候数据是有效的(并且,确保对象的确是不可变的,这样数据就不会在之后变成无效的状态)。由于零做分母对Rational来说是无效状态,因此在把零传递给d的时候,务必不能让Rational被构建出来。
解决这个问题的最好办法是为主构造器定义一个先决条件:precondition说明d必须为非零值。先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。一种方式是使用require方法:
1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 override def toString = n +"/"+ d 4 }
require方法带一个布尔型参数。如果传入的值为真,require将正常返回。反之,require将通过抛出IllegalArgumentException来阻止对象被构造。
6.5 添加字段
现在主构造器可以正确地执行先决条件,我们将把注意力集中到支持加法。想做到这点,我们将在类Rational上定义一个公开的add方法,它带另一个Rational做参数。为了保持Rational不可变,add方法必须不能把传入的分数加到自己身上。而是必须创建并返回一个全新的带有累加值的Rational。你或许想你可以这么写add:
1 class Rational(n: Int, d: Int) { // 编译不过 require(d != 0) 2 override def toString = n +"/"+ d 3 def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d) 4 }
很不幸,上面的代码会让编译器提示说:
1 <console>:11: error: value d is not a member of Rational new Rational(n * that.d + that.n * d, d * that.d) ˆ 2 <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的n和d,需要把它们放在字段中。代码6.1展示了如何把这些字段加入类Rational。
在代码6.1展示的Rational版本里,我们增加了两个字段,分别是numer和denom,并用类参数n和d初始化它们。我们还改变了toString和add的实现,让它们使用字段,而不是类参数。类Rational的这个版本能够编译通过,可以通过分数的加法测试它:
1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 val numer: Int = n 4 val denom: Int = d 5 override def toString = numer+"/"+denom 6 def add(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) 7 }
1 scala> val oneHalf = new Rational(1, 2) 2 oneHalf: Rational = 1/2 3 scala> val twoThirds = new Rational(2, 3) 4 twoThirds: Rational = 2/3 5 scala> oneHalf add twoThirds 6 res0: Rational = 7/6
另一件之前不能而现在可以做的事是在对象外面访问分子和分母。只要访问公共的numer和denom字段即可:
1 scala> val r = new Rational(1, 2) 2 r: Rational = 1 / 2 3 scala> r.numer 4 res7: Int = 1 5 scala> r.denom 6 res8: Int = 2
6.6 自指向
关键字this指向当前执行方法被调用的对象实例,或者如果使用在构造器里的话,就是正被构建的对象实例。例如,我们考虑添加一个方法,lessThan,来测试给定的分数是否小于传入的参数:
1 def lessThan(that: Rational) = this.numer * that.denom < that.numer * this.denom
这里,this.numer指向lessThan被调用的那个对象的分子。你也可以去掉this前缀而只是写numer;着两种写法是相同的。 举一个不能缺少this的例子,考虑在Rational类里添加max方法返回指定分数和参数中的较大者:
1 def max(that: Rational) = if (this.lessThan(that)) that else this
这里,第一个this是冗余的,你写成(lessThan(that))也是一样的。但第二个this表示了当测试为假的时候的方法的结果;如果你省略它,就什么都返回不了了。
6.7 从构造器
有些时候一个类里需要多个构造器。Scala里主构造器之外的构造器被称为从构造器:auxiliary constructor。比方说,分母为1的分数只写分子的话就更为简洁。如,对于5/1来说,可以只是写成5。因此,如果不是写成Rational(5, 1),客户程序员简单地写成Rational(5)或许会更好看一些。这就需要给Rational添加一个只带一个参数,分子,的从构造器并预先设定分母为1。代码6.2展示了应该有的样子
1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 val numer: Int = n 4 val denom: Int = d 5 def this(n: Int) = this(n, 1) 6 override def toString = numer+"/"+denom 7 def add(that: Rational): Rational = new Rational( numer * that.denom + that.numer * denom, denom * that.denom ) 8 }
Scala的从构造器开始于def this(...)。Rational的从构造器主体几乎完全是调用主构造器,直接传递了它的唯一的参数,n,作为分子和1作为分母。输入下列代码到解释器里可以实际看到从构造器的效果:
1 scala> val y = new Rational(3) 2 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的分子和分母可能比它所需要的要大。例如分数66/42,可以更约简化为相同的最简形式,11/7,但Rational的主构造器当前并不做这个工作:
1 scala> new Rational(66, 42) 2 res15: Rational = 66/42
要想对分数进行约简化,需要把分子和分母都除以最大公约数:greatest common divisor。如:66和42的最大公约数是6。(另一种说法就是,6是能够除尽66和42的最大的整数。)66/42的分子和分母都除以6就产生它的最简形式,11/7。代码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) }
这个版本的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都将被构造成它的最简形式
1 scala> new Rational(66, 42) 2 res24: Rational = 11/7
6.9 定义操作符
Rational加法的当前实现仅就完成功能来讲是没问题的,但它可以做得更好用。你或许会问你自己为什么对于整数或浮点数你可以写成:
x + y
但是如果是分数就必须写成:
x.add(y)
或至少是:
x add y
没有合理的解释为什么就必须是这样的。分数和别的数应该是一样的。数学的角度上看他们甚至比,唔,浮点数,更自然。为什么就不能使用自然的数学操作符呢?Scala里面你做得到。本章后续部分,我们会告诉你怎么做。 第一步是用通常的数学的符号替换add方法。这可以直接做到,因为Scala里+是合法的标识符。我们可以用+定义方法名。既然已经到这儿了,你可以同样实现一个*方法以实现乘法,结果展示在代码6.4中:
以上是关于Scala编程--函数式对象的主要内容,如果未能解决你的问题,请参考以下文章